├── .prettierignore ├── .eslintignore ├── .prettierrc ├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── api │ └── api.js ├── components │ ├── Avatar │ │ ├── styles.js │ │ └── Avatar.js │ ├── UserProfile │ │ ├── styles.js │ │ └── UserProfile.js │ ├── LoginButton │ │ ├── style.js │ │ └── LoginButton.js │ ├── Globals │ │ └── Globals.js │ ├── UserCard │ │ ├── styles.js │ │ └── UserCard.js │ ├── ApiConsumer │ │ └── ApiConsumer.js │ ├── Timeline │ │ └── Timeline.js │ ├── App │ │ ├── App.js │ │ └── styles.js │ ├── Stories │ │ ├── styles.js │ │ └── Stories.js │ ├── Photo │ │ ├── styles.js │ │ └── Photo.js │ └── Navigation │ │ ├── styles.js │ │ └── Navigation.js ├── index.css ├── index.js ├── theme │ └── index.js ├── providers │ └── AuthProvider.js ├── db │ └── index.js └── registerServiceWorker.js ├── .editorconfig ├── .gitignore ├── README.md ├── .eslintrc.js ├── instructions ├── context.md └── render-props.md └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/registerServiceWorker.js 2 | node_modules 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alejandronanez/instagram-clone/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/api/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const ENDPOINT = 'http://localhost:3001'; 4 | 5 | export const getFromApi = url => axios.get(`${ENDPOINT}/${url}`); 6 | -------------------------------------------------------------------------------- /src/components/Avatar/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const UserPicture = styled.img` 4 | border-radius: 50%; 5 | height: 50px; 6 | width: 50px; 7 | `; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /src/components/UserProfile/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Wrapper = styled.div` 4 | padding-bottom: 10px; 5 | border-bottom: 1px solid ${({ theme }) => theme.pallete.gallery}; 6 | `; 7 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif; 3 | } 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | body { 10 | background-color: #fafafa; 11 | } 12 | 13 | p { 14 | margin: 0; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/LoginButton/style.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Button = styled.button` 4 | background-color: ${({ theme }) => theme.pallete.mineShaft}; 5 | border: none; 6 | color: ${({ theme }) => theme.pallete.white}; 7 | margin-top: 20px; 8 | padding: 10px 20px; 9 | width: 100%; 10 | `; 11 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | .vscode -------------------------------------------------------------------------------- /src/components/Avatar/Avatar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { UserPicture } from './styles'; 3 | 4 | export class Avatar extends Component { 5 | render() { 6 | return ( 7 | { 9 | console.log('click avatar'); 10 | }} 11 | > 12 | 13 | 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './components/App/App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | import { ThemeProvider } from 'styled-components'; 7 | import theme from 'theme'; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); 15 | registerServiceWorker(); 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # An IG clone using React and Styled Components. 2 | 3 | ## Stack 4 | 5 | * React (Using create react app) 6 | * Styled Components 7 | * Faker 8 | * Json Server 9 | 10 | ## How to run this project? 11 | 12 | * Install Node - Node 8+ will be fine 13 | * `yarn` or `npm install` 14 | * `yarn start` or `npm start` 15 | * **UI:** Open browser in `localhost:3000` 16 | * **Backend:** `localhost:3001/photos` and `localhost:3001/users` 17 | 18 | ## About the workshop: 19 | 20 | * Render Props 21 | * Context Api 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: ['eslint:recommended', 'plugin:react/recommended', 'prettier'], 8 | parser: 'babel-eslint', 9 | parserOptions: { 10 | sourceType: 'module', 11 | }, 12 | plugins: ['react'], 13 | rules: { 14 | indent: ['error', 2], 15 | 'linebreak-style': ['error', 'unix'], 16 | quotes: ['error', 'single'], 17 | semi: ['error', 'always'], 18 | 'no-console': [1], 19 | 'react/prop-types': 0, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/Globals/Globals.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const FlexRow = styled.div` 4 | display: flex; 5 | flex-direction: row; 6 | `; 7 | 8 | export const FlexColumn = styled.div` 9 | display: flex; 10 | flex-direction: column; 11 | `; 12 | 13 | export const MaxWidthContainer = styled.div` 14 | max-width: ${({ theme }) => theme.values.maxWidth}; 15 | margin: 0 auto; 16 | `; 17 | 18 | export const MaxWidthSecondaryContainer = styled.div` 19 | max-width: ${({ theme }) => theme.values.maxSecondaryWidth}; 20 | margin: 0 auto; 21 | `; 22 | -------------------------------------------------------------------------------- /src/components/UserProfile/UserProfile.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Wrapper } from './styles'; 3 | import { UserCard } from 'components/UserCard/UserCard'; 4 | 5 | export class UserProfile extends Component { 6 | render() { 7 | return ( 8 | 9 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/theme/index.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | export const fonts = css`-apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif`; 4 | 5 | const pallete = { 6 | alabaster: '#fafafa', 7 | dustyGray: '#999', 8 | gallery: '#efefef', 9 | mineShaft: '#262626', 10 | silver: '#c7c7c7', 11 | white: '#fff', 12 | }; 13 | 14 | const values = { 15 | maxWidth: '1010px', 16 | maxSecondaryWidth: '935px', 17 | breakpoints: { 18 | small: '768px', 19 | big: '1024px', 20 | }, 21 | }; 22 | 23 | export default { 24 | pallete, 25 | values, 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/LoginButton/LoginButton.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button } from './style'; 3 | import { AuthConsumer } from 'providers/AuthProvider'; 4 | 5 | export class LoginButton extends Component { 6 | render() { 7 | return ( 8 |
9 | 10 | {({ isAuth, toggleAuth }) => ( 11 | 15 | )} 16 | 17 |
18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/providers/AuthProvider.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, Component } from 'react'; 2 | 3 | const { Consumer, Provider } = createContext({ 4 | isAuth: false, 5 | toggleAuth() {}, 6 | }); 7 | 8 | export class AuthProvider extends Component { 9 | state = { 10 | isAuth: false, 11 | }; 12 | 13 | toggleAuth = () => { 14 | this.setState(prevState => ({ isAuth: !prevState.isAuth })); 15 | }; 16 | 17 | render() { 18 | return ( 19 | 20 | {this.props.children} 21 | 22 | ); 23 | } 24 | } 25 | 26 | export const AuthConsumer = Consumer; 27 | -------------------------------------------------------------------------------- /src/components/UserCard/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { FlexRow, FlexColumn } from 'components/Globals/Globals'; 3 | 4 | export const Wrapper = styled(FlexRow)``; 5 | 6 | export const UserInfoWrapper = styled(FlexColumn)` 7 | align-self: center; 8 | font-size: 14px; 9 | margin-left: 10px; 10 | `; 11 | 12 | export const UserName = styled.a` 13 | color: ${({ theme }) => theme.pallete.mineShaft}; 14 | font-weight: 600; 15 | display: block; 16 | 17 | &:hover { 18 | cursor: pointer; 19 | } 20 | `; 21 | 22 | export const UserSubtitle = styled.span` 23 | color: ${({ theme }) => theme.pallete.dustyGray}; 24 | display: block; 25 | font-size: 12px; 26 | margin-top: 2px; 27 | `; 28 | -------------------------------------------------------------------------------- /src/components/UserCard/UserCard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Wrapper, UserInfoWrapper, UserName, UserSubtitle } from './styles'; 3 | import { Avatar } from 'components/Avatar/Avatar'; 4 | 5 | export class UserCard extends Component { 6 | render() { 7 | return ( 8 | 9 | 10 | 11 | { 13 | console.log('username clicked'); 14 | }} 15 | > 16 | {this.props.username} 17 | 18 | {this.props.fullname} 19 | 20 | 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ApiConsumer/ApiConsumer.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import { getFromApi } from 'api/api'; 3 | 4 | export class ApiConsumer extends Component { 5 | state = { 6 | loading: false, 7 | error: '', 8 | data: [], 9 | }; 10 | 11 | async componentDidMount() { 12 | this.setState(() => ({ loading: true })); 13 | 14 | try { 15 | const { data } = await getFromApi(this.props.endpoint); 16 | 17 | this.setState(() => ({ 18 | data, 19 | })); 20 | } catch (e) { 21 | this.setState(() => ({ 22 | error: e.message, 23 | })); 24 | } finally { 25 | this.setState(() => ({ loading: false })); 26 | } 27 | } 28 | 29 | render() { 30 | const { loading, error, data } = this.state; 31 | 32 | return this.props.children({ 33 | loading, 34 | error, 35 | data, 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /instructions/context.md: -------------------------------------------------------------------------------- 1 | # Render Props 2 | 3 | * `git checkout context -f` 4 | * Take a look at `/providers` 5 | * You can use React's Context in order to avoid _prop drilling_ 6 | * React's Context uses the Render Props pattern 7 | 8 | ## Your task 9 | 10 | * Use the `` component that uses React's Context API to update the state of the button bellow `Stories` 11 | * If the user is **logged in**, the button should say: `Logout` 12 | * If the user is **not logged in**, the button should say: `Login` 13 | * If the user is **logged in**, the three icons in the navigation **should be** displayed 14 | * If the user is **not logged in**, the three icons in the navigation should not be displayed 15 | 16 | * The releavant files for this exercise are: 17 | * `/src/providers/AuthProvider/AuthProvider.js`; 18 | * `/src/components/App/App.js`; 19 | * `/src/components/LoginButton/LoginButton.js`; 20 | * `/src/components/Navigation/Navigation.js`; 21 | -------------------------------------------------------------------------------- /instructions/render-props.md: -------------------------------------------------------------------------------- 1 | # Render Props 2 | 3 | * `git checkout render-props -f` 4 | * Create a component that: 5 | * Let you query any endpoint 6 | * Pass `loading` and `error` states to other components 7 | 8 | ## Steps 9 | 10 | * Create a component called `ApiConsumer` 11 | * `ApiConsumer` should receive a prop called `endpoint` `` for example. 12 | * `ApiConsumer` should handle all the request states, **Loading**, **Error** and **Data** 13 | * The response from the API should be stored inside **Data** 14 | * The component should work like 15 | 16 | ```js 17 | 18 | {({ loading, error, data }) => { 19 | // do what you need with loading 20 | // Show loading state... 21 | // do what you need with error 22 | // Show Error state... 23 | // do what you need with data 24 | // Show the right data with what is returned by the API 25 | }} 26 | 27 | ``` 28 | -------------------------------------------------------------------------------- /src/components/Timeline/Timeline.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Photo } from 'components/Photo/Photo'; 3 | import { ApiConsumer } from 'components/ApiConsumer/ApiConsumer'; 4 | 5 | export class Timeline extends Component { 6 | renderPhotos = photos => { 7 | return photos.map(photo => ( 8 | 16 | )); 17 | }; 18 | 19 | render() { 20 | return ( 21 |
22 | 23 | {({ loading, error, data }) => { 24 | if (loading) { 25 | return

Loading timeline...

; 26 | } 27 | 28 | if (error) { 29 | return

{error.message}

; 30 | } 31 | 32 | return this.renderPhotos(data); 33 | }} 34 |
35 |
36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { Navigation } from 'components/Navigation/Navigation'; 3 | import { UserProfile } from 'components/UserProfile/UserProfile'; 4 | import { Stories } from 'components/Stories/Stories'; 5 | import { Timeline } from 'components/Timeline/Timeline'; 6 | import { LoginButton } from 'components/LoginButton/LoginButton'; 7 | import { AuthProvider } from 'providers/AuthProvider'; 8 | import { ContentContainer, TimelineWrapper, UpdatesContainer } from './styles'; 9 | 10 | class App extends Component { 11 | render() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | } 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /src/components/App/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { 3 | MaxWidthSecondaryContainer, 4 | FlexColumn, 5 | } from 'components/Globals/Globals'; 6 | import { Wrapper as PhotoWrapper } from 'components/Photo/styles'; 7 | 8 | export const ContentContainer = styled(MaxWidthSecondaryContainer)` 9 | display: flex; 10 | flex-direction: row; 11 | margin-top: 60px; 12 | padding-bottom: 60px; 13 | 14 | ${PhotoWrapper} { 15 | &:first-child { 16 | margin-top: 0; 17 | } 18 | 19 | margin-top: 20px; 20 | } 21 | `; 22 | 23 | export const TimelineWrapper = styled(FlexColumn)` 24 | margin-left: auto; 25 | margin-right: auto; 26 | max-width: 614px; 27 | width: 100%; 28 | 29 | @media (min-width: ${({ theme }) => theme.values.breakpoints.big}) { 30 | margin-left: 0; 31 | margin-right: 24px; 32 | } 33 | `; 34 | 35 | export const UpdatesContainer = styled(FlexColumn)` 36 | display: none; 37 | height: 100vh; 38 | max-width: 293px; 39 | width: 100%; 40 | 41 | @media (min-width: ${({ theme }) => theme.values.breakpoints.big}) { 42 | display: block; 43 | } 44 | `; 45 | -------------------------------------------------------------------------------- /src/components/Stories/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { FlexRow } from 'components/Globals/Globals'; 3 | 4 | export const Wrapper = styled.section` 5 | margin-top: 16px; 6 | `; 7 | 8 | export const TitleWrapper = styled(FlexRow)` 9 | align-items: center; 10 | justify-content: space-between; 11 | `; 12 | 13 | export const Title = styled.h1` 14 | color: ${({ theme }) => theme.pallete.dustyGray}; 15 | font-weight: 600; 16 | font-size: 14px; 17 | margin: 0; 18 | `; 19 | 20 | export const TitleCTA = styled.a` 21 | color: ${({ theme }) => theme.pallete.mineShaft}; 22 | font-size: 12px; 23 | font-weight: 600; 24 | 25 | &:hover { 26 | cursor: pointer; 27 | } 28 | `; 29 | 30 | export const FriendsStories = styled.section` 31 | border-bottom: 1px solid ${({ theme }) => theme.pallete.gallery}; 32 | box-shadow: inset 0 -10px 10px -10px rgb(220, 220, 220); 33 | height: 306px; 34 | margin-top: 10px; 35 | overflow-y: scroll; 36 | 37 | > div { 38 | &:first-child { 39 | margin-top: 0; 40 | } 41 | 42 | &:last-child { 43 | margin-bottom: 15px; 44 | } 45 | 46 | margin-top: 15px; 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /src/db/index.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker'); 2 | const getImageUrl = imageId => 3 | `https://picsum.photos/2000/2000/?image=${imageId}`; 4 | 5 | function getPhotos(totalPhotos = 20) { 6 | return Array.from({ length: totalPhotos }).map(() => ({ 7 | id: faker.random.uuid(), 8 | likes: faker.random.number({ min: 0, max: 500 }), 9 | liked: faker.random.boolean(), 10 | bookmarked: faker.random.boolean(), 11 | photo: getImageUrl(faker.random.number({ min: 1, max: 999 })), 12 | user: { 13 | id: faker.random.uuid(), 14 | name: `${faker.name.firstName()} ${faker.name.lastName()}`, 15 | username: faker.internet.userName().toLowerCase(), 16 | avatar: faker.internet.avatar(), 17 | }, 18 | })); 19 | } 20 | 21 | function getUsers(totalUsers = 50) { 22 | return Array.from({ length: totalUsers }).map(() => ({ 23 | id: faker.random.uuid(), 24 | name: `${faker.name.firstName()} ${faker.name.lastName()}`, 25 | username: faker.internet.userName().toLowerCase(), 26 | avatar: faker.internet.avatar(), 27 | })); 28 | } 29 | 30 | module.exports = () => { 31 | return { 32 | photos: getPhotos(), 33 | users: getUsers(), 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "instagram-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.18.0", 7 | "classnames": "^2.2.6", 8 | "cross-env": "^5.1.6", 9 | "npm-run-all": "^4.1.3", 10 | "react": "^16.4.0", 11 | "react-dom": "^16.4.0", 12 | "react-scripts": "1.1.4", 13 | "styled-components": "^3.3.0" 14 | }, 15 | "scripts": { 16 | "start:server": "json-server src/db/index.js --port 3001", 17 | "start:ui": "react-scripts start", 18 | "start": 19 | "cross-env NODE_PATH=src npm-run-all --parallel start:ui start:server", 20 | "build": "cross-env react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject", 23 | "precommit": "lint-staged", 24 | "eslint": "eslint src" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^4.19.1", 28 | "eslint-config-prettier": "^2.9.0", 29 | "eslint-plugin-react": "^7.8.2", 30 | "faker": "^4.1.0", 31 | "husky": "^0.14.3", 32 | "json-server": "^0.14.0", 33 | "lint-staged": "^7.1.2", 34 | "prettier": "1.12.1" 35 | }, 36 | "lint-staged": { 37 | "*.{js,json,css,md}": ["prettier --write", "git add"] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Photo/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { FlexRow } from 'components/Globals/Globals'; 3 | 4 | export const Wrapper = styled.div` 5 | background-color: ${({ theme }) => theme.pallete.white}; 6 | border-radius: 5px; 7 | border: 1px solid ${({ theme }) => theme.pallete.gallery}; 8 | `; 9 | 10 | export const Author = styled.div` 11 | padding: 10px; 12 | `; 13 | 14 | export const UserPhoto = styled.div``; 15 | 16 | export const Actions = styled(FlexRow)` 17 | justify-content: space-between; 18 | `; 19 | 20 | export const IGIcon = styled.i` 21 | font-size: 23px; 22 | 23 | &.heart-liked { 24 | color: red; 25 | } 26 | 27 | &.bookmarked { 28 | color: ${({ theme }) => theme.pallete.mineShaft}; 29 | } 30 | `; 31 | 32 | export const ActionIconWrapper = styled.div` 33 | padding-bottom: 10px; 34 | ${IGIcon} { 35 | margin-right: 16px; 36 | 37 | &:last-child { 38 | margin-right: 0; 39 | } 40 | } 41 | `; 42 | 43 | export const UserActions = styled.div` 44 | background-color: ${({ theme }) => theme.pallete.white}; 45 | padding: 20px; 46 | `; 47 | 48 | export const Likes = styled.div` 49 | font-size: 14px; 50 | font-weight: 700; 51 | `; 52 | 53 | export const Timestamp = styled.div``; 54 | -------------------------------------------------------------------------------- /src/components/Stories/Stories.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Wrapper, 4 | Title, 5 | TitleWrapper, 6 | TitleCTA, 7 | FriendsStories, 8 | } from './styles'; 9 | import { UserCard } from 'components/UserCard/UserCard'; 10 | import { ApiConsumer } from 'components/ApiConsumer/ApiConsumer'; 11 | 12 | export class Stories extends Component { 13 | renderCard = storyData => { 14 | return storyData.map(story => ( 15 | 21 | )); 22 | }; 23 | 24 | render() { 25 | return ( 26 | 27 | 28 | Stories 29 | Watch all 30 | 31 | 32 | {({ loading, error, data }) => { 33 | if (loading) { 34 | return

Loading stories...

; 35 | } 36 | 37 | if (error) { 38 | return

{error}

; 39 | } 40 | 41 | return {this.renderCard(data)}; 42 | }} 43 |
44 |
45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Photo/Photo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import cx from 'classnames'; 3 | import { UserCard } from 'components/UserCard/UserCard'; 4 | import { 5 | Wrapper, 6 | Author, 7 | UserPhoto, 8 | Actions, 9 | Likes, 10 | IGIcon, 11 | ActionIconWrapper, 12 | UserActions, 13 | } from './styles'; 14 | 15 | export class Photo extends Component { 16 | render() { 17 | const { bookmarked, liked, likes, photo, user } = this.props; 18 | const heartClass = cx('fa-heart', { 19 | 'fas heart-liked': liked, 20 | fal: !liked, 21 | }); 22 | 23 | const bookmarkClass = cx('fa-bookmark', { 24 | 'fas bookmarked': bookmarked, 25 | fal: !bookmarked, 26 | }); 27 | 28 | return ( 29 | 30 | 31 | 36 | 37 | 38 | {`Photo 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {likes} likes 49 | 50 | 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Navigation/styles.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | import { MaxWidthContainer } from 'components/Globals/Globals'; 3 | 4 | export const Wrapper = styled.section` 5 | background-color: ${({ theme }) => theme.pallete.white}; 6 | border-bottom: 1px solid ${({ theme }) => theme.pallete.gallery}; 7 | `; 8 | 9 | export const Container = styled(MaxWidthContainer)` 10 | align-items: center; 11 | display: flex; 12 | flex-direction: row; 13 | justify-content: space-between; 14 | padding: 21px 40px; 15 | `; 16 | 17 | export const IGIcon = styled.i` 18 | font-size: 35px; 19 | `; 20 | 21 | export const CTAIcon = styled.i` 22 | display: inline-block; 23 | font-size: 24px; 24 | `; 25 | 26 | export const SearchInput = styled.input` 27 | border: 1px solid ${({ theme }) => theme.pallete.gallery}; 28 | background-color: ${({ theme }) => theme.pallete.alabaster}; 29 | display: none; 30 | min-width: 200px; 31 | transition: min-width 250ms ease, background-color 250ms ease; 32 | padding: 8px 16px; 33 | 34 | &:active, 35 | &:focus { 36 | background-color: ${({ theme }) => theme.pallete.white}; 37 | min-width: 300px; 38 | outline: none; 39 | } 40 | 41 | @media (min-width: ${({ theme }) => theme.values.breakpoints.small}) { 42 | display: block; 43 | } 44 | `; 45 | 46 | export const A = styled.a` 47 | display: inline-block; 48 | margin-left: 20px; 49 | 50 | &:first-child { 51 | margin-left: 0; 52 | } 53 | 54 | &:hover { 55 | cursor: pointer; 56 | } 57 | `; 58 | -------------------------------------------------------------------------------- /src/components/Navigation/Navigation.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { AuthConsumer } from 'providers/AuthProvider'; 3 | import { A, Wrapper, Container, CTAIcon, IGIcon, SearchInput } from './styles'; 4 | 5 | export class Navigation extends Component { 6 | handleSubmit = e => { 7 | e.preventDefault(); 8 | 9 | console.log('Searching...'); 10 | }; 11 | 12 | handleCTAClick = e => { 13 | e.preventDefault(); 14 | 15 | console.log('CTA clicked...'); 16 | }; 17 | 18 | render() { 19 | return ( 20 | 21 | 22 |
23 | 24 |
25 |
26 |
27 | 28 | 29 |
30 | 31 | {({ isAuth }) => { 32 | if (isAuth) { 33 | return ( 34 | 45 | ); 46 | } 47 | }} 48 | 49 |
50 |
51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 15 | 16 | 17 | 26 | React App 27 | 28 | 29 | 30 | 33 |
34 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------