-
Social Media 만들기 - 7) userInfoProfile , edit profile 기능 만들기NODE.JS 2021. 7. 7. 16:31
search.js 의 form 형식을 search버튼을 누르면 작동하도록 이전의 useEffect를 함수로 만들자.
import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { getDataAPI } from '../../utils/fetchData' import { GLOBALTYPES } from '../../redux/actions/globalTypes' import {Link} from 'react-router-dom' import UserCard from '../UserCard' const Search = () => { const [search, setSearch] = useState('') const [users, setUsers] = useState([]) const {auth} = useSelector(state => state) const dispatch = useDispatch() const handleSearch= async (e) =>{ e.preventDefault() if(!search) return; try{ const res = await getDataAPI(`search?username=${search}`, auth.token) setUsers(res.data.users) }catch(err){ dispatch({ type:GLOBALTYPES.ALERT, payload:{error:err.response.data.msg} }) } } const handleClose= () =>{ setSearch('') setUsers([]) } return ( <form className="search_form" onSubmit={handleSearch}> <input type="text" name = "search" value={search} id="search" onChange={e=>setSearch(e.target.value.toLowerCase().replace(/ /g, ''))} /> <div className="search_icon" style={{opacity: search? 0 : 0.3}}> <span className="material-icons">search</span> <span>Search</span> </div> <div className="close_search" style={{opacity: users.length===0? 0:1}} onClick = {handleClose}> × </div> <button type="submit">Search</button> <div className="users"> { search && users.map(user=>( <Link key={user._id} to ={`/profile/${user._id}`} onClick={handleClose}> <UserCard user={user} border = "border"/> </Link> )) } </div> </form> ) } export default Search
display속성을 none으로 만들어서 엔터를치면 나오도록 하자.
<button type="submit" style={{display:'none'}}>Search</button>
loading 이미지를 넣어보자.
import LoadIcon from '../../images/loading.gif' const Search = () => { const [load, setLoad] = useState(false) const handleSearch= async (e) =>{ e.preventDefault() if(!search) return; try{ setLoad(true) const res = await getDataAPI(`search?username=${search}`, auth.token) setUsers(res.data.users) setLoad(false) return ( <button type="submit" style={{display:'none'}}>Search</button> { load && <img src={LoadIcon} alt="loading" /> }
스타일링을 주자.
.header .search_form .loading{ position: absolute; top: 50%; right: 5px; width: 15px; height: 15px; transform: translateY(-50%) ; }
Search.js로 가서 함수와 데이터를 prop으로 넘겨줘서 link to를 userCard에서 하도록 하자.
<div className="users"> { search && users.map(user => ( <UserCard key={user._id} user={user} border="border" handleClose={handleClose} /> )) } </div> </form> ) } export default Search
userCard.js
import React from 'react' import Avatar from './Avatar' import {Link} from 'react-router-dom' const UserCard = ({user, border, handleClose}) => { const handleCloseAll=()=>{ if(handleClose) handleClose() } return ( <div className={`d-flex p-2 align-item-center ${border}`}> <div> <Link to ={`/profile/${user._id}`} onClick={handleCloseAll} className="d-flex align-item-center"> <Avatar src={user.avatar} size="big-avatar"/> <div className="ml-1" style={{transform:'translateY(-2px)'}}> <span className="d-block">{user.username}</span> <small style={{opacity:0.7}}>{user.fullname}</small> </div> </Link> </div> </div> ) } export default UserCard
userctrl가서 get user하는 라우터를 작성한다.
getUser: async (req, res) => { try { const users = await Users.findById(req.param.id).select('-password') if(!user) return res.status(400).json({msg:"User does not exist."}) res.json({user}) } catch (err) { return res.status(500).json({msg: err.message}) } },
userRouter.js에 추가해준다.
const router = require('express').Router() const auth = require("../middleware/auth") const userCtrl = require("../controllers/userCtrl") router.get('/search', auth, userCtrl.searchUser) router.get('/user/:id', auth, userCtrl.getUser) module.exports = router
profileAction.js를 만들고 추가해준다.
import { GLOBALTYPES } from "./globalTypes"; export const PROFILE_TYPES={ LOADING:'LOADING', GET_USER:'GET_USER' }
reducer를 만들어준다.
import { PROFILE_TYPES } from "../actions/profileAction"; const initialState = { loading:false, users:[], posts:[] } const profileReducer = (state=initialState, action)=>{ switch(action.type){ case PROFILE_TYPES.LOADING: return { ...state, loading: action.payload }; case PROFILE_TYPES.GET_USER: return { ...state, users: [...state.users, action.payload.user] }; default: return state; } } export default profileReducer
index.js에 reducer 추가해준다.
import { combineReducers } from "redux"; import auth from './authReducers'; import alert from './alertReducer'; import theme from './themeReducer'; import profile from './profileReducer' export default combineReducers({ auth, alert, theme, profile })
profile>[id].js로가서 다음과 같이 component를 넣어준다.
import React from 'react' import Info from '../../components/profile/Info' import Post from '../../components/profile/Post' const profile = () => { return ( <div className="profile"> <Info /> <Post /> </div> ) } export default profile
component>profile> Info.js, Post.js를 만들어준다.
Info.js
import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useParams, Link } from 'react-router-dom' import Avatar from '../Avatar' const Info = () => { const {id} = useParams() const {auth} = useSelector(state => state) const dispatch = useDispatch() const [userData, setUserData] = useState([]) useEffect(() => { if(id===auth.user._id){ setUserData([auth.user]) } }, [id,auth.user]) return ( <div className="info"> { userData.map(user => ( <div className="info_container" key={user._id}> <Avatar src={user.avatar} size="supper-avatar" /> <div className="info_content"> <div className="info_content_title"> <h2>{user.username}</h2> <button className="btn btn-outline-info">Edit Profile</button> </div> <div> <span> {user.followers.length} Followers </span> <span> {user.following.length} Following </span> </div> <h6>{user.fullname}</h6> <p>{user.address}</p> <h6>{user.email}</h6> <a href={user.website} target="_blank" rel="noreferrer">{user.website}</a> <p>{user.story}</p> </div> </div> )) } </div> ) } export default Info
Info.js
import React, { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useParams, Link } from 'react-router-dom' import Avatar from '../Avatar' const Info = () => { const {id} = useParams() const {auth} = useSelector(state => state) const dispatch = useDispatch() const [userData, setUserData] = useState([]) useEffect(() => { if(id===auth.user._id){ setUserData([auth.user]) } }, [id,auth.user]) return ( <div className="info"> { userData.map(user => ( <div className="info_container" key={user._id}> <Avatar src={user.avatar} size="supper-avatar" /> <div className="info_content"> <div className="info_content_title"> <h2>{user.username}</h2> <button className="btn btn-outline-info">Edit Profile</button> </div> <div className="follow_btn"> <span className="mr-4"> {user.followers.length} Followers </span> <span className="ml-4"> {user.following.length} Following </span> </div> <h6>{user.fullname} {user.mobile}</h6> <p className="m-0">{user.address}</p> <h6>{user.email}</h6> <a href={user.website} target="_blank" rel="noreferrer">{user.website}</a> <p>{user.story}</p> </div> </div> )) } </div> ) } export default Info
profile.css를 추가해준다.
/* ------PROFILE--------- */ @import url("./profile.css");
/* Info */ .info{ width: 100%; max-width: 800px; margin: auto; padding: 20px 10px; } .info_container{ display: flex; justify-content: space-around; flex-wrap: wrap; } .info_content{ min-width: 250px; max-width: 400px; width: 100%; flex: 1; opacity: 0.7; margin: 0 15px; } .info_content_title{ display: flex; align-items: center; flex-wrap: wrap; } .info_content_title h2{ font-size: 2rem; font-weight: 400; transform: translateY(4px); flex: 3; } .info_content_title button{ flex: 2; } .info_container .follow_btn{ color:teal; cursor: pointer; } .info_container .follow_btn span:hover{ text-decoration: underline; }
지금은 로그인한 프로필과 같을 때만 가져오는데,
다른 유저의 정보들도 가져오도록 dispatch해주자.
유저의 정보를 가져오도록 profile.js를 만들어준다.
import React, { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux'; import Info from '../component/Info' import { getProfileUser } from '../_actions/profileActions'; import Avatar from '../component/Avatar' import { PROFILE_GETUSER_RESET } from '../_constants/profileConstants'; import Loading from '../component/Loading'; import Alert from '../component/Alert' function Profile(props) { const userId = props.match.params.id; const userLogin = useSelector(state => state.userLogin) const {userInfo} = userLogin const dispatch = useDispatch() const userProfile = useSelector(state => state.userProfile) const {loading, error, user} = userProfile useEffect(() => { if(!user || userId !== user._id){ dispatch(getProfileUser(userId)) } }, [dispatch, userId, user]) return ( <div className="profile"> {loading&& <Loading></Loading>} {error && <Alert variant="danger">{error}</Alert>} <div className="info"> <div className="info_container" key={user?._id}> <Avatar src={user?.avatar} size="supper-avatar" /> <div classNamåe="info_content"> <div className="info_content_title"> <h2>{user?.username}</h2> { user?._id === userInfo.user._id ? <button className="btn btn-outline-info">Edit Profile</button> : '' } </div> <div> <span> {user?.followers.length} Followers </span> <span> {user?.following.length} Following </span> </div> <h6>{user?.fullname}</h6> <p>{user?.address}</p> <h6>{user?.email}</h6> <a href={user?.website} target="_blank" rel="noreferrer">{user?.website}</a> <p>{user?.story}</p> </div> </div> </div> </div> ) } export default Profile
userRouter에 유저정보 가져오는 라우터 추가해준다.
userRouter.get('/:id',auth, async(req, res)=>{ try{ const user = await User.findById(req.params.id) .select('-password') if(user){ res.send(user) }else{ res.status(404).send({message:'User Not Found.'}) } }catch(err){ return res.status(500).json({message: err.message}) } })
profile constant
export const PROFILE_GETUSER_REQUEST = 'PROFILE_GETUSER_REQUEST' export const PROFILE_GETUSER_SUCCESS = 'PROFILE_GETUSER_SUCCESS' export const PROFILE_GETUSER_FAIL = 'PROFILE_GETUSER_FAIL' export const PROFILE_GETUSER_RESET = 'PROFILE_GETUSER_RESET'
profileActions.js
import axios from "axios" import { PROFILE_GETUSER_FAIL, PROFILE_GETUSER_REQUEST, PROFILE_GETUSER_SUCCESS } from "../_constants/profileConstants" export const getProfileUser = (userId)=>async (dispatch, getState)=>{ dispatch({ type:PROFILE_GETUSER_REQUEST, payload:userId }) const {userLogin:{userInfo}} = getState() try{ const {data} = await axios.get(`/api/users/${userId}`,{headers:{authorization : `Bearer ${userInfo?.token}`} }) dispatch({ type:PROFILE_GETUSER_SUCCESS, payload:data }) }catch (err){ dispatch({ type:PROFILE_GETUSER_FAIL, payload:{error:err.response && err.response.data.message? err.response.data.message : err.message} }) } }
profile Reducer.js
import { PROFILE_GETUSER_FAIL, PROFILE_GETUSER_REQUEST, PROFILE_GETUSER_RESET, PROFILE_GETUSER_SUCCESS } from "../_constants/profileConstants"; export const getUserProfileReducer = (state={loading:true}, action)=>{ switch(action.type){ case PROFILE_GETUSER_REQUEST: return {loading:true}; case PROFILE_GETUSER_SUCCESS: return {loading:false, user:action.payload} case PROFILE_GETUSER_FAIL: return {loading:false, error:action.payload} case PROFILE_GETUSER_RESET: return {loading:true} default: return state; } }
store.js
const reducer = combineReducers({ userLogin:userLoginReducer, userRegister:userRegisterReducer, alert:alertReducer, theme:themeReducer, userProfile:getUserProfileReducer, })
profile.css
.profile{ width: 100%; min-height: 100vh; } /* ------ Info ---------- */ .info{ width: 100%; max-width: 800px; margin: auto; padding: 20px 10px; } .info_container{ display: flex; justify-content: space-around; flex-wrap: wrap; } .info_content{ min-width: 250px; max-width: 550px; width: 100%; flex: 1; opacity: 0.7; margin: 0 15px; } .info_content_title{ display: flex; align-items: center; flex-wrap: wrap; } .info_content_title h2{ font-size: 2rem; font-weight: 400; transform: translateY(4px); flex: 3; } .info_content_title button{ flex: 2; } .info_container .follow_btn{ color: teal; cursor: pointer; } .info_container .follow_btn span:hover{ text-decoration: underline; } /* -------------- Profile Edit ------------ */ .edit_profile{ position: fixed; top:0; left: 0; width: 100%; height: 100vh; background: #0008; z-index: 9; overflow: auto; } .edit_profile form{ max-width: 450px; width: 100%; background: white; padding: 20px; border-radius: 5px; margin: 20px auto; } .edit_profile .btn_close{ position: absolute; top: 1rem; right: 1rem; } .edit_profile .info_avatar{ width: 150px; height: 150px; overflow: hidden; border-radius: 50%; position: relative; margin: 15px auto; border: 1px solid #ddd; cursor: pointer; } .edit_profile .info_avatar img{ width: 100%; height: 100%; display: block; object-fit: cover; } .edit_profile .info_avatar span{ position: absolute; bottom: -100%; left: 0; width: 100%; height: 50%; text-align: center; color: orange; transition: 0.3s ease-in-out; background: #fff5; } .edit_profile .info_avatar:hover span{ bottom: -15%; } .edit_profile .info_avatar #file_up{ position: absolute; top:0; left: 0; width: 100%; height: 100%; cursor: pointer; opacity: 0; } ::-webkit-file-upload-button{ cursor: pointer; } /* ----------- Follow ------- */ .follow{ position: fixed; top:0; left: 0; background: #0008; width: 100%; height: 100vh; z-index: 4; } .follow_box{ width: 350px; border: 1px solid #222; border-radius: 5px; background: white; padding: 20px 10px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .follow_content{ width: 100%; height: 350px; overflow: auto; } .follow_box .close{ position: absolute; top: 0; right: 3px; font-size: 3rem; cursor: pointer; } /* --------- Save Tab ----------- */ .profile_tab{ width: 100%; display: flex; justify-content: center; border-top: 1px solid #eee; border-bottom: 1px solid #eee; } .profile_tab button{ outline: none; border: none; background: white; text-transform: uppercase; font-weight: bold; font-size: 1.2rem; padding: 5px 20px; margin: 0 25px; opacity: 0.5; } .profile_tab button.active{ border-top: 1px solid #000; border-bottom: 1px solid #000; opacity: 0.8; }
global.css에 추가해준다.
/* -------- Profile ---------- */ @import url("./profile.css");
이제 edit profile 버튼을 활성화 시키자.
profile.js에 editprofile.js컴포넌트를 넣어주는데, x 표시를 활성화 하기 위해서 setOnEdit을 prop으로 넘겨준다.
<span> {user?.followers.length} Followers </span> <span> {user?.following.length} Following </span> </div> { onEdit && <EditProfile setOnEdit = {setOnEdit}/> }
editprofile.js
여기서 아바타는 theme가 변경되어도 변화 없게끔 설정해줬다.
파일 업로더를 이용해서 설정했다.
import React from 'react' import { useState } from 'react' import { useSelector } from 'react-redux' import Alert from '../component/Alert' function EditProfile({setOnEdit}) { const userLogin = useSelector(state => state.userLogin) const {userInfo} = userLogin const theme = useSelector(state => state.theme) const [fullname, setFullname] = useState(userInfo.user.fullname) const [mobile, setMobile] = useState(userInfo.user.mobile) const [address, setAddress] = useState(userInfo.user.address) const [website, setWebsite] = useState(userInfo.user.website) const [story, setStory] = useState(userInfo.user.story) const [gender, setGender] = useState(userInfo.user.gender) const [avatar, setAvatar] = useState('') const changeAvatar=(e) =>{ const file = e.target.files[0] const err = checkImage(file) if(err)return window.alert(err) setAvatar(file) } const checkImage = (file)=>{ let err = "" if(!file) return err = "File does not exist." if(file.size> 1024*1024){ err = "The alrgest image size is 1mb." } if(file.type !=='image/jpeg' && file.type !== 'image/png'){ err = "Image format is incorrect." } return err } const handleSubmit = (e) =>{ e.preventDefault() } return ( <div className="edit_profile"> <button className="btn btn-danger btn_close" onClick={()=>setOnEdit(false)}> Close </button> <form onSubmit={handleSubmit}> <div className="info_avatar"> <img src={avatar? URL.createObjectURL(avatar):userInfo.user.avatar} alt="avatar" style={{filter:theme? 'invert(1)':'inver(0)'}}/> <span> <i className="fas fa-camera"></i> <p>Change</p> <input type="file" name="file" id="file_up" accept="image/*" onChange={changeAvatar}/> </span> </div> <div className="form-group"> <label htmlFor="fullname">Full Name</label> <div className="position-relative"> <input type="text" className="form-control" id="fullname" name="fullname" onChange={e=>setFullname(e.target.value)} value={fullname} required/> <small className="text-danger position-absolute" style={{top:'50%', right:'5px', transform:'translateY(-50%)'}}> {fullname.length}/25 </small> </div> </div> <div className="form-group"> <label htmlFor="mobile">Mobile</label> <input type="text" className="form-control" id="mobile" name="mobile" onChange={e=>setMobile(e.target.value)} value={mobile}/> </div> <div className="form-group"> <label htmlFor="address">Address</label> <input type="address" className="form-control" id="address" name="address" onChange={e=>setAddress(e.target.value)} value={address} /> </div> <div className="form-group"> <label htmlFor="website">Website</label> <input type="text" className="form-control" id="website" name="website" onChange={e=>setWebsite(e.target.value)} value={website} /> </div> <div className="form-group"> <label htmlFor="story">Story</label> <textarea name="story" id="story" cols="30" rows="4" value={story} onChange={(e)=>setStory(e.target.value)}></textarea> <small className="text-danger d-block text-right" > {story.length}/200 </small> </div> <label htmlFor="gender">Gender</label> <div className="input-group-prepend px-0 mb-4"> <select className="custom-select text-capitalize" name="gender" id="gender" value={gender} onChange={e=>setGender(e.target.value)}> <option value="male">Male</option> <option value="female">Female</option> <option value="other">Other</option> </select> </div> <button className="btn btn-info w-100" type="submit"> Save </button> </form> </div> ) } export default EditProfile
사진 너무 크거나 오류나는 사진 올리면,
오류메세지 뜨면서 업로드 안된다. edit profile router를 작성해주자.
userRouter.put('/:id', auth, async(req, res)=>{ try{ const user = await User.findById(req.params.id) if(user){ user.fullname = req.body.fullname || user.fullname user.avatar = req.body.avatar || user.avatar user.mobile = req.body.mobile || user.mobile user.address = req.body.address || user.address user.website = req.body.website || user.website user.story = req.body.story || user.story user.gender = req.body.gender || user.gender } const updatedUser = await user.save() res.send({ user:updatedUser, token:generateToken(updatedUser) }) }catch(err){ return res.status(500).json({message:err.message}) } })
edit profile action
export const updateUserProfile = (user ) =>async (dispatch, getState)=>{ dispatch({ type:USER_UPDATE_PROFILE_REQUEST, payload:{user} }) const {userLogin: {userInfo}} = getState() try{ const {data} = await axios.put(`/api/users/${userInfo.user._id}`, user, { headers: {authorization : `Bearer ${userInfo.token}`} }) dispatch({ type:USER_UPDATE_PROFILE_SUCCESS, payload : data }) // dispatch({ // type:USER_LOGIN_SUCCESS, // payload:{ // token:userInfo.token, // user:{data} // } // }) dispatch({ type:USER_LOGIN_SUCCESS, payload:data }) localStorage.removeItem('userInfo') localStorage.setItem('userInfo',JSON.stringify(data)) }catch(err){ dispatch({ type:USER_UPDATE_PROFILE_FAIL, payload:{error:err.response && err.response.data.message? err.response.data.message : err.message} }) } }
edit profile reducer
export const userUpdateProfileReducer = (state={}, action)=>{ switch(action.type){ case USER_UPDATE_PROFILE_REQUEST: return {loading:true} case USER_UPDATE_PROFILE_SUCCESS: return {loading:false, success:true} case USER_UPDATE_PROFILE_FAIL: return {loading:false, error:action.payload} case USER_UPDATE_PROFILE_RESET: return {} default: return state } }
store.js
const reducer = combineReducers({ userLogin:userLoginReducer, userRegister:userRegisterReducer, alert:alertReducer, theme:themeReducer, userProfile:getUserProfileReducer, userUpdateProfile:userUpdateProfileReducer, })
'NODE.JS' 카테고리의 다른 글
Cast to ObjectId failed for value "" at path "_id" 오류 (0) 2021.07.10 Social Media 만들기 - 8) follow, unfollow 기능 만들기 (0) 2021.07.10 Social Media 만들기 - 6) search 기능 만들기 (Header) (0) 2021.07.06 Social Media 만들기 - 5) logout 기능 만들기 (Header) (0) 2021.07.06 Social Media 만들기 - 4) Register 기능 만들기 (0) 2021.07.06