React/React Clone Coding

[Tinder Clone Coding]틴더 클론 코딩

정현수 2020. 8. 30. 17:17
반응형

이 글은 저 혼자 " Clever Programmer " 채널의 Clone Coding 영상을 보고 혼자 따라해본 글입니다.

 

Clever Programmer

You can find awesome programming lessons here! Also, expect programming tips and tricks that will take your coding skills to the next level.

www.youtube.com


 

 

내 TINDER 클론 코딩 깃허브 주소

github.com/junghyeonsu/tinder-clone

구조 파악 (0분 ~ 11분)

import React, { Component } from 'react';
import Header from './Header';
import './App.css';

class App extends Component {
  render(){
    return (
      <div className="App">
        <h1>This is awesome TINDER Clone App</h1>

        {/* Header */}
        <Header />

        {/* Tinder Cards */}
        {/* Buttons belos tinder cards */}

        {/* Chats screen */}
        {/* Individual Chat screen */}

      </div>
    );
  }
}

export default App;

 

Header 컴포넌트 생성(11분 ~ 48분)

App.js

import React, { Component } from 'react';
import Header from './Header';
import './App.css';
import { firestore } from "./firebase";
import { Button } from 'react-bootstrap';

class App extends Component {
  render(){
    return (
      <div className="App">
        <Header />

        {/* Tinder Cards */}
        {/* Buttons belos tinder cards */}

        {/* Chats screen */}
        {/* Individual Chat screen */}

      </div>
    );
  }
}

export default App;

 

 

Header.js

import React from 'react'
import "./Header.css";
import PersonIcon from '@material-ui/icons/Person';
import ForumIcon from '@material-ui/icons/Forum';
import IconButton from "@material-ui/core/IconButton";

function Header() {
    return (
        <div className="header">
            <IconButton>
                <PersonIcon className="header__icon" fontSize="large" />
            </IconButton>
            <img
                className="header__logo" 
                src="https://1000logos.net/wp-content/uploads/2018/07/tinder-logo.png" 
                alt="tinder logo" 
            />
            <IconButton>
                <ForumIcon className="header__icon" fontSize="large" />
            </IconButton>
        </div>
    )
}

export default Header

 

Header.css

.header {
    /* display to row */
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid #f9f9f9;
}

.header__logo {
    height: 40px;
    
    /*내용이 종횡비를 유지하면서 요소에 정의된 너비와 높이안에서 가능한한 많이 확대(scale up)시킨다*/
    object-fit: contain;
}

.header__icon {
    margin: 20px;
}

Header 완성

 

TinderCards 컴포넌트 생성 및 Router 연결 (48분 ~ 1시간 30분)

 

App.js

import React, { Component } from 'react';
import Header from './Header';
import TinderCards from './TinderCards';
import { BrowserRouter as Router, Switch, Route, Link} from "react-router-dom";
import './App.css';
import { firestore } from "./firebase";

class App extends Component {
  render(){
    return (
      <div className="App">
        <Header />
        <Router>
          <Switch>
            <Route path="/chat">
              <h1>I am the Chatpage</h1>
            </Route>
            <Route path="/">
              <TinderCards />
            </Route>
          </Switch>
          {/* Tinder Cards */}
          {/* Buttons belos tinder cards */}

          {/* Chats screen */}
          {/* Individual Chat screen */}

        </Router>
      </div>
    );
  }
}

export default App;

 

TinderCards.js

import React, { useState } from 'react'
import TinderCard from "react-tinder-card";
import './TinderCards.css';

function TinderCards() {
    const [people, setPeople] = useState([
        {
            name: 'steve jobs',
            url: 'https://www.biography.com/.image/t_share/MTY2MzU3OTcxMTUwODQxNTM1/steve-jobs--david-paul-morrisbloomberg-via-getty-images.jpg'
        },
        {
            name: 'mark zuckerberg',
            url: 'https://api.time.com/wp-content/uploads/2019/04/mark-zuckerberg-time-100-2019.jpg?quality=85&zoom=2'
        }
    ]);

    return (
        <div>
            <h1>Tinder Card</h1>

            <div className="tinderCards__cardContainer">

            {people.map(person => {
                return(
                <TinderCard
                    className="swipe"
                    key={person.name}
                    preventSwipe={['up', 'down']}
                >
                    <div 
                        className="card"
                        style={{ backgroundImage:`url(${person.url})` }}
                    >
                        <h3>{person.name}</h3>
                    </div>
                </TinderCard>
                );
            })}
            </div>
        </div>
    );
}

export default TinderCards;

 

TinderCards.css

.card {
    position: relative;
    width: 600px;
    padding: 20px;
    max-width: 85vw;
    height: 50vh;
    border-radius: 20px;
    background-size: cover;
    background-position: center;
    box-shadow: 0px 18px 53px 0px rgba(0, 0, 0, 0.3);
}

.card > h3 {
    position: absolute;
    bottom: 10px;
    color: white;
}

.tinderCards__cardContainer {
    display: flex;
    justify-content: center;
    margin-top: 5vh;
}

.swipe {
    position: absolute;
}

 

 

TinderCard 완성

 

Firebase 연동 (1시간 30분 ~ 2시간)

firebase에 name과, url의 필드를 갖는 document를 추가해줌으로써

react에서 불러와서 사용할 수 있게 연동을 했습니다.

 

import database from './firebase';

	useEffect(() => {
        
        database.collection('people').onSnapshot(snapshot => {
            setPeople(snapshot.docs.map(doc => doc.data()))
        })

    }, []);

database는 firebase에서 import해오고

collection의 이름은 저희는 people로 지정을 해줬고,

onSnapshot은 말그대로 데이터베이스를 사진을 찍어서 보여준다는 느낌으로 생각하면 됩니다.

그리고 setPeople을 통해서 people state를 snapshot의 document로 저장해줍니다.

 

그러면 render 부분에서 name과 url을 보고 사진과 이름을 띄우는 형식입니다.

 

TinderCards.js

import React, { useState, useEffect } from 'react'
import database from './firebase';
import TinderCard from "react-tinder-card";
import './TinderCards.css';

function TinderCards() {
    const [people, setPeople] = useState([]);

    useEffect(() => {
        
        database.collection('people').onSnapshot(snapshot => {
            setPeople(snapshot.docs.map(doc => doc.data()))
        })

    }, []);

    return (
        <div>
            <h1>Tinder Card</h1>

            <div className="tinderCards__cardContainer">

            {people.map(person => {
                return(
                <TinderCard
                    className="swipe"
                    key={person.name}
                    preventSwipe={['up', 'down']}
                >
                    <div 
                        className="card"
                        style={{ backgroundImage:`url(${person.url})` }}
                    >
                        <h3>{person.name}</h3>
                    </div>
                </TinderCard>
                );
            })}
            </div>
        </div>
    );
}

export default TinderCards;

 

SwipeButtons 컴포넌트 생성 (2시간 ~ 2시간 20분)

 

 

화면 아래의 버튼들을 SwipeButtons 컴포넌트에 넣어주고 스타일링을 해줬습니다.

 

SwipeButtons.js

import React from 'react'

import ReplayIcon from "@material-ui/icons/Replay";
import CloseIcon from "@material-ui/icons/Close";
import StarRateIcon from "@material-ui/icons/StarRate";
import FavoriteIcon from "@material-ui/icons/Favorite";
import FlashOnIcon from "@material-ui/icons/FlashOn";
import IconButton from "@material-ui/core/IconButton";

import './SwipeButtons.css';

const SwipeButtons = () => {
    return (
        <div className="swipeButtons">
            <IconButton className="swipeButtons__repeat">
                <ReplayIcon fontSize="large" />
            </IconButton>
            <IconButton className="swipeButtons__left">
                <CloseIcon fontSize="large" />
            </IconButton >
            <IconButton className="swipeButtons__star">
                <StarRateIcon fontSize="large" />
            </IconButton>
            <IconButton className="swipeButtons__right">
                <FavoriteIcon fontSize="large" />
            </IconButton>
            <IconButton className="swipeButtons__lightning">
                <FlashOnIcon fontSize="large" />
            </IconButton>
        </div>
    )
}

export default SwipeButtons;

 

SwipeButtons.css

.swipeButtons {
    position: fixed;
    bottom: 2vh;
    width: 100%;
    display: flex;
    justify-content: space-evenly;
}

.swipeButtons .MuiIconButton-root {
    background-color: white;
    box-shadow: 0px 10px 53px 0px rgba(0, 0, 0, 0.3) !important;
}

.swipeButtons__repeat {
    padding: 3vw !important;
    color: #ec5e6f !important;
}

.swipeButtons__left {
    padding: 3vw !important;
    color: #ec5e6f !important;
}

.swipeButtons__star {
    padding: 3vw !important;
    color: #62b4f9 !important;
}

.swipeButtons__right {
    padding: 3vw !important;
    color: #76e2b3 !important;
}

.swipeButtons__lightning {
    padding: 3vw !important;
    color: #915dd1 !important;
}

 

 

메세지 화면 이동 및 메세지 컴포넌트 구현(2시간 20분 ~ 3시간)

 

 

홈 화면에서 메세지 버튼 클릭하면 왼쪽 위의 버튼이 뒤로가기 버튼으로 바뀌는 것도 구현

 

그리고 Chats 컴포넌트에서 Chat 컴포넌트를 추가해서 메세지 화면을 구현

 

Chats 와 Chat은 다른 컴포넌트입니다!

 

Chats.js

import React from 'react'
import Chat from './Chat';
import './Chats.css';

function Chats() {
    return (
        <div className="chats">
            <Chat
                name="Mark"
                message="Yo whats up!"
                timestamp="40 seconds age"
                profilePic="..."
            />
            <Chat
                name="Mark"
                message="Yo whats up!"
                timestamp="40 seconds age"
                profilePic="..."
            />
            <Chat
                name="Mark"
                message="Yo whats up!"
                timestamp="40 seconds age"
                profilePic="..."
            />
            <Chat
                name="Mark"
                message="Yo whats up!"
                timestamp="40 seconds age"
                profilePic="..."
            />
        </div>
    )
}

export default Chats

 

Chat.js

import React from 'react'
import Avatar from "@material-ui/core/Avatar"
import "./Chat.css";

function Chat({ name, message, profilePic, timestamp }) {
    return (
        <div className="chat">
            <Avatar className="chat__image" alt={name} src={profilePic} />
            <div className="chat__details">
                <h3>{name}</h3>
                <p>{message}</p>
            </div>
            <p className="chat__timestamp">{timestamp}</p>
        </div>
    )
}

export default Chat

 

Chat.css

.chat {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 20px;
    height: 70px;
    border-bottom: 1px solid rgb(236, 236, 236);
}

.chat__details{
    flex: 1;
}

.chat__details > p {
    color: gray;
}

.chat__timestamp {
    color: lightgray;
}

.chat__image {
    margin-right: 20px;
}

 

바뀐 Header.js

import React from 'react'
import "./Header.css";
import PersonIcon from '@material-ui/icons/Person';
import ForumIcon from '@material-ui/icons/Forum';
import IconButton from "@material-ui/core/IconButton";
import ArrowBackIosIcon from "@material-ui/icons/ArrowBackIos";
import { Link, useHistory } from "react-router-dom";

function Header({ backButton }) {
    const history = useHistory();
    return (
        <div className="header">
            {backButton ? (
                <IconButton onClick={() => history.replace(backButton)}>
                    <ArrowBackIosIcon fontSize="large" className="header__icon" />
                </IconButton>    
            ): (
                <IconButton>
                    <PersonIcon className="header__icon" fontSize="large" />
                </IconButton>
            )}
            <Link to="/">
                <img
                    className="header__logo" 
                    src="https://1000logos.net/wp-content/uploads/2018/07/tinder-logo.png" 
                    alt="tinder logo" 
                />
            </Link>
            <Link to="/chat">
                <IconButton>
                    <ForumIcon className="header__icon" fontSize="large" />
                </IconButton>
            </Link>
        </div>
    )
}

export default Header

 

App.js

import React, { Component } from 'react';
import Header from './Header';
import TinderCards from './TinderCards';
import SwipeButtons from './SwipeButtons';
import Chats from './Chats';
import { BrowserRouter as Router, Switch, Route, Link} from "react-router-dom";
import './App.css';
import { firestore } from "./firebase";

class App extends Component {
  render(){
    return (
      <div className="App">
        <Router>
          <Switch>
            <Route path="/chat">
              <Header backButton="/" />
              <Chats />
            </Route>
            <Route path="/">  
              <Header />
              <TinderCards />
              <SwipeButtons />
            </Route>
          </Switch>
          {/* Tinder Cards */}
          {/* Buttons belos tinder cards */}

          {/* Chats screen */}
          {/* Individual Chat screen */}

        </Router>
      </div>
    );
  }
}

export default App;

 

ChatScreen 컴포넌트 구현 & 라우터 적용 & 채팅 기능 구현 & 마무리 (3시간 00분 ~ 3시간 50분)

 

CSS가 도중에 안먹는 오류가 있어서 잘안됐습니다.

그대로 따라쳤는데 안됨...

 

여튼 클론 코딩 마무리 !

 

전체코드

더보기

App.js

import React, { Component } from 'react';
import Header from './Header';
import TinderCards from './TinderCards';
import SwipeButtons from './SwipeButtons';
import Chats from './Chats';
import ChatScreen from './ChatScreen';
import { BrowserRouter as Router, Switch, Route, Link} from "react-router-dom";
import './App.css';
import { firestore } from "./firebase";

class App extends Component {
  render(){
    return (
      <div className="App">
        <Router>
          <Switch>
            <Route path="/chat/:person">
              <Header backButton="/chat" />
              <ChatScreen />
            </Route>
            <Route path="/chat">
              <Header backButton="/" />
              <Chats />
            </Route>
            <Route path="/">  
              <Header />
              <TinderCards />
              <SwipeButtons />
            </Route>
          </Switch>
        </Router>
      </div>
    );
  }
}

export default App;

 

Chats.js

import React from 'react'
import Chat from './Chat';
import './Chats.css';

function Chats() {
    return (
        <div className="chats">
            <Chat
                name="Mark"
                message="Yo whats up!"
                timestamp="40 seconds age"
                profilePic="..."
            />
            <Chat
                name="Mark"
                message="Yo whats up!"
                timestamp="40 seconds age"
                profilePic="..."
            />
            <Chat
                name="Mark"
                message="Yo whats up!"
                timestamp="40 seconds age"
                profilePic="..."
            />
            <Chat
                name="Mark"
                message="Yo whats up!"
                timestamp="40 seconds age"
                profilePic="..."
            />
        </div>
    )
}

export default Chats

 

Chat.js

import React from 'react'
import Avatar from "@material-ui/core/Avatar"
import "./Chat.css";
import { Link } from 'react-router-dom';

function Chat({ name, message, profilePic, timestamp }) {
    return (
        <Link to ={`/chat/${name}`}>
            <div className="chat">
                <Avatar className="chat__image" alt={name} src={profilePic} />
                <div className="chat__details">
                    <h3>{name}</h3>
                    <p>{message}</p>
                </div>
                <p className="chat__timestamp">{timestamp}</p>
            </div>
        </Link>
    )
}

export default Chat

 

Chat.css

.chat {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 20px;
    height: 70px;
    border-bottom: 1px solid rgb(236, 236, 236);
}

a {
    text-decoration: none;
    color: inherit;
}

.chat__details{
    flex: 1;
}

.chat__details > p {
    color: gray;
}

.chat__timestamp {
    color: lightgray;
}

.chat__image {
    margin-right: 20px;
}

 

ChatScreen.js

import React, { useState } from 'react'
import Avatar from "@material-ui/core/Avatar"
import './ChatScreen.css';

function ChatScreen() {
    const [input, setInput] = useState('');
    const [messages, setMessages] = useState([
        {
            name: 'Elien',
            image: '...',
            message: 'Whats up'
        },
        {
            name: 'Elien',
            image: '...',
            message: 'hows it going!'
        },
        {
            message: 'hows it going!'
        },
    ])

    const handleSend = e => {
        e.preventDefault();
        setMessages([...messages, { message : input }]);
        setInput('');
    }

    return (
        <div className="chatScreen">
            <p className="chatScreen__timestamp">YOU MATCHED WITH MARK ON 10/08/20</p>
            {messages.map(message => 
                message.name ? (
                <div className="chatScreen__message">
                    <Avatar 
                        className="chatScreen__image" 
                        alt={message.name} 
                        src={message.image} 
                    />
                    <p className="chatScreen__text"> {message.message} </p>
                </div>
                ) : (
                <div className="chatScreen__message">
                    <p className="chatScreen__textUser"> {message.message} </p>
                </div>
                )
            )}

            <div className="chatScreen__input">
                <form>
                    <input
                        value={input}
                        onChange={(e) => setInput(e.target.value)} 
                        className="chatScreen__inputField"
                        placeholder="메세지를 입력하세요"
                        type="text"
                        />
                    <button 
                        className="chatScreen__inputButton"
                        onClick={handleSend}    
                    >SEND</button>
                </form>
            </div>
        </div>
    )
}

export default ChatScreen

 

ChatScreen.css

.chatScreen__message {
    display: flex;
    align-items: center;
    padding: 20px;
}

.chatScreen__text {
    margin-left: 10px;
    background-color: lightgray;
    padding: 15px;
    border-radius: 20px;
}

.chatScreen__timestamp {
    text-align: center;
    padding: 20px;
    color: gray;
}

.chatScreen__textUser {
    margin-left: auto;
    background-color: #29b3cd;
    padding: 15px;
    border-radius: 20px;
    color: white;
}

.chatScreen__input {
    display: flex;
    padding: 5px;
    position: fixed;
    bottom: 0;
    width: 100%;
    border-top: 1px solid lightgray;
}

.chatScreen__inputField {
    flex: 1;
    padding: 10px;
    border: none;
}

.chatScreen__inputButton {
    border: none;
    margin-right: 20px;
    background-color: white;
    font-weight: bolder;
    color: #fe3d71;
}

 

firebase.js

import firebase from "firebase/app";
import "firebase/firestore";

const firebaseConfig = {
    apiKey: "-",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: "",
    appId: "",
    measurementId: ""
  };

// 내가 찾은 것
// firebaseConfig 정보로 firebase 시작
// firebase.initializeApp(firebaseConfig);
// firebase의 firestore 인스턴스를 변수에 저장
// const firestore = firebase.firestore();

// 틴더 클론 코딩에서
const firebaseApp = firebase.initializeApp(firebaseConfig);
const database = firebaseApp.firestore();

// 필요한 곳에서 사용할 수 있도록 내보내기
export default database;

 

Header.js

import React from 'react'
import "./Header.css";
import PersonIcon from '@material-ui/icons/Person';
import ForumIcon from '@material-ui/icons/Forum';
import IconButton from "@material-ui/core/IconButton";
import ArrowBackIosIcon from "@material-ui/icons/ArrowBackIos";
import { Link, useHistory } from "react-router-dom";

function Header({ backButton }) {
    const history = useHistory();
    return (
        <div className="header">
            {backButton ? (
                <IconButton onClick={() => history.replace(backButton)}>
                    <ArrowBackIosIcon fontSize="large" className="header__icon" />
                </IconButton>    
            ): (
                <IconButton>
                    <PersonIcon className="header__icon" fontSize="large" />
                </IconButton>
            )}
            <Link to="/">
                <img
                    className="header__logo" 
                    src="https://1000logos.net/wp-content/uploads/2018/07/tinder-logo.png" 
                    alt="tinder logo" 
                />
            </Link>
            <Link to="/chat">
                <IconButton>
                    <ForumIcon className="header__icon" fontSize="large" />
                </IconButton>
            </Link>
        </div>
    )
}

export default Header

 

Header.css

.header {
    /* display to row */
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid #f9f9f9;
}

.header__logo {
    height: 40px;
    
    /*내용이 종횡비를 유지하면서 요소에 정의된 너비와 높이안에서 가능한한 많이 확대(scale up)시킨다*/
    object-fit: contain;
}

.header__icon {
    margin: 20px;
}

 

SwipeButtons.js

import React from 'react'

import ReplayIcon from "@material-ui/icons/Replay";
import CloseIcon from "@material-ui/icons/Close";
import StarRateIcon from "@material-ui/icons/StarRate";
import FavoriteIcon from "@material-ui/icons/Favorite";
import FlashOnIcon from "@material-ui/icons/FlashOn";
import IconButton from "@material-ui/core/IconButton";

import './SwipeButtons.css';

const SwipeButtons = () => {
    return (
        <div className="swipeButtons">
            <IconButton className="swipeButtons__repeat">
                <ReplayIcon fontSize="large" />
            </IconButton>
            <IconButton className="swipeButtons__left">
                <CloseIcon fontSize="large" />
            </IconButton >
            <IconButton className="swipeButtons__star">
                <StarRateIcon fontSize="large" />
            </IconButton>
            <IconButton className="swipeButtons__right">
                <FavoriteIcon fontSize="large" />
            </IconButton>
            <IconButton className="swipeButtons__lightning">
                <FlashOnIcon fontSize="large" />
            </IconButton>
        </div>
    )
}

export default SwipeButtons;

 

SwipeButtons.css

.swipeButtons {
    position: fixed;
    bottom: 2vh;
    width: 100%;
    display: flex;
    justify-content: space-evenly;
}

.swipeButtons .MuiIconButton-root {
    background-color: white;
    box-shadow: 0px 10px 53px 0px rgba(0, 0, 0, 0.3) !important;
}

.swipeButtons__repeat {
    padding: 3vw !important;
    color: #ec5e6f !important;
}

.swipeButtons__left {
    padding: 3vw !important;
    color: #ec5e6f !important;
}

.swipeButtons__star {
    padding: 3vw !important;
    color: #62b4f9 !important;
}

.swipeButtons__right {
    padding: 3vw !important;
    color: #76e2b3 !important;
}

.swipeButtons__lightning {
    padding: 3vw !important;
    color: #915dd1 !important;
}

 

TinderCards.js

import React, { useState, useEffect } from 'react'
import database from './firebase';
import TinderCard from "react-tinder-card";
import './TinderCards.css';

function TinderCards() {
    const [people, setPeople] = useState([]);

    useEffect(() => {
        
        database.collection('people').onSnapshot(snapshot => {
            setPeople(snapshot.docs.map(doc => doc.data()))
        })

    }, []);

    return (
        <div>
            <div className="tinderCards__cardContainer">

            {people.map(person => {
                return(
                <TinderCard
                    className="swipe"
                    key={person.name}
                    preventSwipe={['up', 'down']}
                >
                    <div 
                        className="card"
                        style={{ backgroundImage:`url(${person.url})` }}
                    >
                        <h3>{person.name}</h3>
                    </div>
                </TinderCard>
                );
            })}
            </div>
        </div>
    );
}

export default TinderCards;

 

TinderCards.css

.card {
    position: relative;
    width: 600px;
    padding: 10px;
    max-width: 85vw;
    height: 55vh;
    border-radius: 20px;
    background-size: cover;
    background-position: center;
    box-shadow: 0px 18px 53px 0px rgba(0, 0, 0, 0.3);
}

.card > h3 {
    position: absolute;
    bottom: 10px;
    color: white;
}

.tinderCards__cardContainer {
    display: flex;
    justify-content: center;
    margin-top: 5vh;
}

.swipe {
    position: absolute;
}

 

 

 

반응형