본문 바로가기

트위터(React, TypeScript, Firebase, Vite)

13. TWEEING - Post Tweet Form

 

 

 

 

 

 

 

 

 

 

 

App.tsx

import { useEffect, useState } from 'react'
import { createBrowserRouter, RouterProvider} from 'react-router-dom';
import Layout from './components/layout';
import Home from './routes/home';
import Profile from './routes/profile';
import Login from './routes/login';
import CreateAccount from './routes/create-account';
import styled, { createGlobalStyle } from 'styled-components';
import reset from "styled-reset";
import LoadingScreen from './components/loading-screen';

import { auth } from './routes/firebase';
import ProtectedRoute from './routes/protected-route';

const Wrapper = styled.div`
  height: 100vh;                          /* 화면 전체 높이를 기준으로 중앙 정렬 */ 
  display: flex;
  justify-content: center;                /* 수평 중앙 */
  align-items: center;                    /* 수직 중앙 */
  flex-direction: column;
  margin: 0 auto;                         /* 화면 가운데로 */
  padding: 50px 0px;
`;

const router = createBrowserRouter([
  {
    path:"/",
    element:
    /*
      <Layout />,
    */
    <Wrapper>
      <ProtectedRoute>
        <Layout />
      </ProtectedRoute>
    </Wrapper>,  
    children: [
      {
        path: "",
        element: 
       /*  
        <ProtectedRoute>
          <Home />
        </ProtectedRoute> 
        */
        <Home />
      },
      {
        path: "profile",
        element:
        /* 
        <ProtectedRoute>
          <Profile />
        </ProtectedRoute>
        */
        <Profile />
      }
    ]
  },
  {
    path: "/login",
    element: <Login />
  },
  {
    path:"/create-account",
    element: <CreateAccount />
  }
]);

const GlobalStyles = createGlobalStyle`
   ${reset};
   * {
     box-sizing: border-box;
   }
   body {
     background-color: black;
     color:white;
     font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
   }
 `;

function App() {

  const [isLoading, setLoading] = useState(true);
  const init = async() => {
    
    await auth.authStateReady();
              // Firebase가 쿠키와 토큰을 읽고 백엔드와 소통해서 로그인여부를 확인하는 동안 기다림
    setLoading(false);
  };

  useEffect(()=>{
    init();
  },[]);
   
  return <>
    <GlobalStyles />
    {isLoading ? <LoadingScreen /> : <RouterProvider router = {router} />}
  </>;
}

export default App

 

protected-route.tsx

 

import React from "react";
import { auth } from "./firebase";
import { Navigate } from "react-router-dom";

export default function ProtectedRoute({children} : {
    children : React.ReactNode;
}){

    const user = auth.currentUser;
    if(user === null){
        return <Navigate to="/login" />
    }
    return children;
}

 

layout.tsx

import { Link, Outlet, useNavigate } from "react-router-dom";
import styled from "styled-components";
import { auth } from "../routes/firebase";

/*
    SVG vector icon 찾을 수 있는 웹사이트(리액트 프로젝트에서 바로 쓸 수 있는 아이콘 찾기 가능)
    https://heroicons.dev/
*/

const Wrapper = styled.div`
    display: grid;
    gap:20px;
    grid-template-columns: 1fr 4fr;
    height: 100%;
    padding: 50px 0px;
    width: 100%;
    max-width: 860px;
`;

const Menu = styled.div`
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 20px;
`;

const MenuItem = styled.div`
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 2px solid white;
    height: 50px;
    width: 50px;
    border-radius: 50%;
    svg{
        width : 30px;
        fill: white;
    }
    &.log-out{
        border-color : tomato;
        svg{
            fill: tomato;
        }
    }
`;

export default function Layout(){
    const navigate = useNavigate();
    const onLogOut = async() => {
        const ok = confirm("Are you sure you want to log out?");
        if(ok){
            await auth.signOut();
            navigate("/login");
        }
    };  

    return(
        <Wrapper>
            <Menu>
                <Link to="/">
                    <MenuItem>
                        <svg data-slot="icon" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
                            <path clip-rule="evenodd" fill-rule="evenodd" d="M9.293 2.293a1 1 0 0 1 1.414 0l7 7A1 1 0 0 1 17 11h-1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-6H3a1 1 0 0 1-.707-1.707l7-7Z"></path>
                        </svg>
                    </MenuItem>
                </Link>
                
                <Link to="/profile">
                    <MenuItem>
                        <svg data-slot="icon" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
                            <path d="M10 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6ZM3.465 14.493a1.23 1.23 0 0 0 .41 1.412A9.957 9.957 0 0 0 10 18c2.31 0 4.438-.784 6.131-2.1.43-.333.604-.903.408-1.41a7.002 7.002 0 0 0-13.074.003Z" />
                        </svg>
                    </MenuItem>
                </Link>

                
                <MenuItem onClick={onLogOut} className="log-out">
                    <svg data-slot="icon" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
                        <path clip-rule="evenodd" fill-rule="evenodd" d="M3 4.25A2.25 2.25 0 0 1 5.25 2h5.5A2.25 2.25 0 0 1 13 4.25v2a.75.75 0 0 1-1.5 0v-2a.75.75 0 0 0-.75-.75h-5.5a.75.75 0 0 0-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 0 0 .75-.75v-2a.75.75 0 0 1 1.5 0v2A2.25 2.25 0 0 1 10.75 18h-5.5A2.25 2.25 0 0 1 3 15.75V4.25Z"></path>
                        <path clip-rule="evenodd" fill-rule="evenodd" d="M19 10a.75.75 0 0 0-.75-.75H8.704l1.048-.943a.75.75 0 1 0-1.004-1.114l-2.5 2.25a.75.75 0 0 0 0 1.114l2.5 2.25a.75.75 0 1 0 1.004-1.114l-1.048-.943h9.546A.75.75 0 0 0 19 10Z"></path>
                    </svg>
                </MenuItem>
                
            </Menu>
            <Outlet />
        </Wrapper>
    )
}

 

home.tsx

import styled from "styled-components";
import PostTweetForm from "../components/post-tweet-form";

const Wraaper = styled.div`
`;

export default function Home(){
  
    return <Wraaper>
        <PostTweetForm />            
    </Wraaper>
}

 

post-tweet-form.tsx

import { useState } from "react";
import styled from "styled-components"

const Form = styled.form`
    display: flex;
    flex-direction: column;
    gap: 10px;
`;

const TextArea = styled.textarea`
   border: 2px solid white;
   padding: 20px;
   border-radius: 20px;
   font-size: 16px;
   color: white;
   background-color: black;
   width: 100%;
   resize: none;
   font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
     Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
   &::placeholder {
     font-size: 16px;
   }
   &:focus {
     outline: none;
     border-color: #1d9bf0;
   }
 `;

const AttachFileButton = styled.label`
   padding: 10px 0px;
   color: #1d9bf0;
   text-align: center;
   border-radius: 20px;
   border: 1px solid #1d9bf0;
   font-size: 14px;
   font-weight: 600;
   cursor: pointer;
 `;
 
 const AttachFileInput = styled.input`
   display: none;
 `;
 
 const SubmitBtn = styled.input`
   background-color: #1d9bf0;
   color: white;
   border: none;
   padding: 10px 0px;
   border-radius: 20px;
   font-size: 16px;
   cursor: pointer;
   &:hover,
   &:active {
     opacity: 0.9;
   }
 `;

export default function PostTweetForm(){
    const [isLoading, setLoading] = useState(false);
    const [tweet, setTweet] = useState("");
    const [file, setFile] = useState<File|null>(null);
    const onChange = (e : React.ChangeEvent<HTMLTextAreaElement>) => {
        setTweet(e.target.value);
    };
    const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const {files} = e.target;
        if(files && files.length === 1){
            setFile(files[0]);
        }
    };

    return <Form>
        <TextArea onChange={onChange} value={tweet} placeholder="What is happening?"/>
        <AttachFileButton htmlFor="file">{file ? "Photo added" : "Add photo"}</AttachFileButton>
        <AttachFileInput onChange={onFileChange} type="file" id="file" accept="image/*"/>
        <SubmitBtn type="submit" value={isLoading ? "Posting..." : "Post Tweet"}/>
    </Form>
};

 

 

 

 

 

 

 

 

 

 

nwitter-reloaded.z01
19.53MB
nwitter-reloaded.z02
19.53MB
nwitter-reloaded.z03
19.53MB
nwitter-reloaded.z04
19.53MB
nwitter-reloaded.z05
19.53MB
nwitter-reloaded.zip
14.61MB

'트위터(React, TypeScript, Firebase, Vite)' 카테고리의 다른 글

15. TWEETING - Uploading Images  (0) 2025.04.17
14. Tweeting to Firestore  (0) 2025.04.16
12. TWEETING - Navigation Bar  (0) 2025.04.15
11. Authentication - Github Login  (0) 2025.04.15
10. Authentication - Log In  (0) 2025.04.15