-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #428 from bounswe/3design_frontend_user_search_res…
…ults 3design frontend user search results
- Loading branch information
Showing
3 changed files
with
236 additions
and
78 deletions.
There are no files selected for viewing
219 changes: 141 additions & 78 deletions
219
3Design/frontend/src/components/SearchResults/SearchResults.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,79 +1,142 @@ | ||
import React, { useEffect, useState } from 'react' | ||
import styles from "./SearchResults.module.css"; | ||
import { useParams } from 'react-router-dom'; | ||
import { DPost } from '../interfaces'; | ||
import { Skeleton } from 'antd'; | ||
import GalleryPost from '../GalleryPost/Clickable/GalleryPost'; | ||
import DiscussionPost from '../DiscussionPost/Clickable/DiscussionPost'; | ||
import SideBar from '../SideBar/SideBar'; | ||
import PageHeader from '../PageHeader/PageHeader'; | ||
import axios from 'axios'; | ||
|
||
const SearchResults = () => { | ||
const {query} = useParams(); | ||
const [searchResults, setSearchResults] = useState<DPost[]>([]); | ||
const [searchLoading, setSearchLoading] = useState(true); | ||
|
||
if (query == undefined || query == null){ | ||
window.location.href = "/home"; | ||
} | ||
|
||
useEffect(() => { | ||
fetchPostData(); | ||
}, []) | ||
|
||
const fetchPostData = async () => { | ||
try{ | ||
const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/v1/posts?param=${query}`, { | ||
headers: { | ||
Authorization: `Bearer ${localStorage.getItem("jwt_token")}` | ||
} | ||
}); | ||
setSearchResults(res.data); | ||
} | ||
catch(e){ | ||
|
||
} | ||
finally{ | ||
setSearchLoading(false); | ||
} | ||
|
||
} | ||
|
||
|
||
return ( | ||
<> | ||
<PageHeader/> | ||
<div className='flex'> | ||
<SideBar active=""/> | ||
<div className={`flex flex-col gap-4 ${styles.mainContainer} p-4`}> | ||
<div className='flex flex-col gap-6'> | ||
<div> | ||
<p className='font-bold text-xl'>Results for: {query}</p> | ||
</div> | ||
</div> | ||
{ searchLoading ? | ||
( | ||
<div className={styles.spinnerContainer}> | ||
<Skeleton active avatar paragraph={{ rows: 4 }} /> | ||
</div> | ||
) : | ||
searchResults.length == 0 ? | ||
<p>No results for this query</p> : | ||
searchResults.map((item, index) => ( | ||
item.isVisualPost ? | ||
<GalleryPost key={`g_${item.isVisualPost}`} postData={item}/> | ||
: | ||
<DiscussionPost key={`d_${item.isVisualPost}`} postData={item} /> | ||
)) | ||
} | ||
</div> | ||
|
||
</div> | ||
|
||
</> | ||
|
||
) | ||
} | ||
|
||
import React, { useEffect, useState } from 'react' | ||
import styles from "./SearchResults.module.css"; | ||
import { useParams, useSearchParams } from 'react-router-dom'; | ||
import { DPost } from '../interfaces'; | ||
import { Pagination, Skeleton } from 'antd'; | ||
import GalleryPost from '../GalleryPost/Clickable/GalleryPost'; | ||
import DiscussionPost from '../DiscussionPost/Clickable/DiscussionPost'; | ||
import SideBar from '../SideBar/SideBar'; | ||
import PageHeader from '../PageHeader/PageHeader'; | ||
import axios from 'axios'; | ||
import UserDisplayer from './UserDisplayer/UserDisplayer'; | ||
|
||
interface UserResponse{ | ||
userId : number; | ||
email : string; | ||
nickName : string; | ||
profilePictureUrl : string; | ||
experience : number; | ||
isFollowed : boolean; | ||
} | ||
|
||
const SearchResults = () => { | ||
const {query} = useParams(); | ||
const [searchResults, setSearchResults] = useState<DPost[]>([]); | ||
const [searchLoading, setSearchLoading] = useState(true); | ||
const [searchType, setSearchType] = useState(true); | ||
const [userResults, setUserResults] = useState<UserResponse[]>([]); | ||
|
||
const [searchParams] = useSearchParams(); | ||
const passedPageNumber = searchParams.get("p") ?? ""; | ||
let pageNumber = 1; | ||
if (/^\d+$/.test(passedPageNumber)){ | ||
pageNumber = parseInt(passedPageNumber); | ||
} | ||
const pageSize = 3; | ||
|
||
if (query == undefined || query == null){ | ||
window.location.href = "/home"; | ||
} | ||
|
||
useEffect(() => { | ||
fetchData(); | ||
}, [searchType]) | ||
|
||
const fetchData = async () => { | ||
setSearchLoading(true); | ||
if (!searchType){ | ||
try{ | ||
const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/v1/users/search?keyword=${query}`, { | ||
headers: { | ||
Authorization: `Bearer ${localStorage.getItem("jwt_token")}` | ||
} | ||
}); | ||
setUserResults(res.data); | ||
} | ||
catch(e){ | ||
|
||
} | ||
finally{ | ||
setSearchLoading(false); | ||
return; | ||
} | ||
} | ||
try{ | ||
const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/v1/posts?param=${query}`, { | ||
headers: { | ||
Authorization: `Bearer ${localStorage.getItem("jwt_token")}` | ||
} | ||
}); | ||
setSearchResults(res.data); | ||
} | ||
catch(e){ | ||
|
||
} | ||
finally{ | ||
setSearchLoading(false); | ||
} | ||
|
||
} | ||
|
||
const searchTypeHandler = (x: boolean) => { | ||
if (searchType == x){ | ||
return; | ||
} | ||
setSearchType(x); | ||
} | ||
|
||
|
||
return ( | ||
<> | ||
<PageHeader/> | ||
<div className='flex'> | ||
<SideBar active=""/> | ||
<div className={`flex flex-col gap-4 ${styles.mainContainer} p-4`}> | ||
<div className='flex flex-col gap-6'> | ||
<div> | ||
<p className='font-bold text-xl'>Results for: {query}</p> | ||
</div> | ||
<div className='flex gap-8 justify-start'> | ||
<button className='btn btn-neutral' style={!searchType ? {background: "#ffffff", color: "black"} : {background: "#d0d0d0", color: "black"}} onClick={() => searchTypeHandler(true)}>Posts</button> | ||
<button className='btn btn-neutral' style={searchType ? {background: "#ffffff", color: "black"} : {background: "#d0d0d0", color: "black"}} onClick={() => searchTypeHandler(false)}>Users</button> | ||
</div> | ||
</div> | ||
{ searchLoading ? | ||
( | ||
<div className={styles.spinnerContainer}> | ||
<Skeleton active avatar paragraph={{ rows: 4 }} /> | ||
</div> | ||
) : | ||
searchType ? | ||
( | ||
searchResults.length == 0 ? | ||
<p>No post results for this query</p> : | ||
<> | ||
{searchResults.slice((pageNumber-1)*pageSize, (pageNumber)*pageSize).map((item, index) => ( | ||
item.isVisualPost ? | ||
<GalleryPost key={`g_${item.postId}`} postData={item}/> | ||
: | ||
<DiscussionPost key={`d_${item.postId}`} postData={item} /> | ||
)) | ||
} | ||
<Pagination align='center' pageSize={pageSize} current={pageNumber} total={searchResults.length} onChange={(x)=>window.location.href = `/search/${query}?p=${x}`}/> | ||
</> | ||
) : | ||
( | ||
userResults.length == 0 ? | ||
<p>No user results for this query</p> : | ||
userResults.map((item, index) => ( | ||
<UserDisplayer data={item}/> | ||
)) | ||
) | ||
} | ||
</div> | ||
|
||
</div> | ||
|
||
</> | ||
|
||
) | ||
} | ||
|
||
export default SearchResults |
10 changes: 10 additions & 0 deletions
10
3Design/frontend/src/components/SearchResults/UserDisplayer/UserDisplayer.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
|
||
.userCard{ | ||
background-color: white; | ||
padding: 20px; | ||
width: 100%; | ||
border-radius: 8px; | ||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1), 0 6px 20px rgba(0, 0, 0, 0.1); | ||
display: flex; | ||
gap: 20px; | ||
} |
85 changes: 85 additions & 0 deletions
85
3Design/frontend/src/components/SearchResults/UserDisplayer/UserDisplayer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { Avatar } from '@mui/material'; | ||
import { Button, message } from 'antd'; | ||
import axios from 'axios'; | ||
import React, { useState } from 'react' | ||
import styles from "./UserDisplayer.module.css" | ||
interface UserResponse{ | ||
userId : number; | ||
email : string; | ||
nickName : string; | ||
profilePictureUrl : string; | ||
experience : number; | ||
isFollowed : boolean; | ||
} | ||
|
||
interface Props{ | ||
data: UserResponse | ||
} | ||
|
||
const UserDisplayer = ({data}: Props) => { | ||
const [stateData, setStateData] = useState<UserResponse>(data); | ||
const [followRequesting, setFollowRequesting] = useState(false); | ||
|
||
const handleFollowLogic = async (e:any,type:boolean) => { | ||
e.stopPropagation(); | ||
setFollowRequesting(true); | ||
if (type){ | ||
try{ | ||
await axios.post(`${process.env.REACT_APP_API_URL}/api/v1/users/follow`, | ||
null, | ||
{ | ||
headers: {Authorization: `Bearer ${localStorage.getItem("jwt_token")}`}, | ||
params: {followedUserId: stateData.userId} | ||
} | ||
); | ||
setStateData(prev => ({...prev, isFollowed: true})); | ||
} | ||
catch(e){ | ||
message.error("Couldn't follow user."); | ||
setStateData(prev => ({...prev, isFollowed: false})); | ||
} | ||
finally{ | ||
setFollowRequesting(false); | ||
return; | ||
} | ||
} | ||
try{ | ||
await axios.delete(`${process.env.REACT_APP_API_URL}/api/v1/users/unfollow`, | ||
{ | ||
headers: {Authorization: `Bearer ${localStorage.getItem("jwt_token")}`}, | ||
params: {followedUserId: stateData.userId} | ||
} | ||
); | ||
setStateData(prev => ({...prev, isFollowed: false})); | ||
} | ||
catch(e){ | ||
message.error("Couldn't unfollow user."); | ||
setStateData(prev => ({...prev, isFollowed: true})); | ||
} | ||
finally{ | ||
setFollowRequesting(false); | ||
return; | ||
} | ||
} | ||
|
||
return ( | ||
<div onClick={() => window.location.href = `/profile/${stateData.userId}`} className={`clickable-post ${styles.userCard}`}> | ||
<div className='flex gap-4 items-center'> | ||
<Avatar src={stateData.profilePictureUrl}/> | ||
<p>{stateData.nickName}</p> | ||
</div> | ||
<div className='mr-0 ml-auto items-center'> | ||
|
||
{ | ||
stateData.userId != parseInt(localStorage.getItem("user_id") ?? "-1") && | ||
(stateData.isFollowed ? | ||
<Button disabled={followRequesting} onClick={(e) => handleFollowLogic(e,false)} className='mr-0 ml-auto'>Unfollow</Button> : | ||
<Button disabled={followRequesting} type='primary' onClick={(e) => handleFollowLogic(e,true)} className='mr-0 ml-auto'>Follow</Button> | ||
) | ||
} | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
export default UserDisplayer |