├── .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 | toggleAuth()}>
12 | {isAuth && 'Logout'}
13 | {!isAuth && 'Login'}
14 |
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 |
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 |
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 |
31 | You need to enable JavaScript to run this app.
32 |
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 |
--------------------------------------------------------------------------------