Skip to content

Commit

Permalink
Merge pull request #305 from techeer-sv/FE/#290
Browse files Browse the repository at this point in the history
Fe/#290 프로젝트 구인 목록 페이지 퍼블리싱 및 알림기능 구현
  • Loading branch information
Yujin-Baek committed Nov 28, 2023
2 parents d9bbc1f + c18be56 commit 01a421e
Show file tree
Hide file tree
Showing 22 changed files with 1,071 additions and 15 deletions.
10 changes: 9 additions & 1 deletion frontend/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@
"rules": {
"react/react-in-jsx-scope": "off",
"import/no-extraneous-dependencies": "off",
"react-hooks/exhaustive-deps": "off"
"react-hooks/exhaustive-deps": "off",
"jsx-a11y/label-has-associated-control": [
2,
{
"required": {
"some": ["nesting", "id"]
}
}
]
}
}
7 changes: 3 additions & 4 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.421.0",
"@hookform/resolvers": "^3.3.1",
"@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0",
"@toast-ui/editor-plugin-color-syntax": "^3.1.0",
"@toast-ui/react-editor": "^3.2.3",
"@tanstack/react-query": "^4.35.3",
"@testing-library/react": "^14.0.0",
"@toast-ui/editor-plugin-code-syntax-highlight": "^3.1.0",
Expand All @@ -41,11 +38,13 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.46.2",
"react-icons": "^4.11.0",
"react-quill": "^2.0.0",
"react-select": "^5.8.0",
"recoil": "^0.7.7",
"recoil-persist": "^5.1.0",
"short-uuid": "^4.2.2",
"sass": "^1.68.0",
"short-uuid": "^4.2.2",
"swiper": "^10.3.0",
"tailwindcss": "3.3.3",
"typescript": "5.2.2",
Expand Down
4 changes: 4 additions & 0 deletions frontend/public/images/svg/tag_x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <div className="flex justify-center">OnBoarding Page</div>
}
3 changes: 1 addition & 2 deletions frontend/src/app/project/(read)/post/[postId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export default function ReadingPage({ params }: ParamsType) {
function toModify() {
router.push('/project/modification')
}

// GET요청 보내서 데이터 가져오고 받은 데이터 변수에 넣어주는 함수
async function getData() {
const res = await fetch(
Expand Down Expand Up @@ -108,8 +109,6 @@ export default function ReadingPage({ params }: ParamsType) {
},
)

router.push('/')

if (!res.ok) {
const error = await res.json()

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/project/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export default function ProjectMain() {
<div>
{data.pages.map((group, i) => (
<div
className="relative mx-8 flex flex-wrap justify-center pt-6 sm:pt-8"
className="mx-8 flex justify-center pt-6 sm:pt-8"
key={group[i]?.id}
>
{group.map((item: DataObject) => (
Expand Down
157 changes: 157 additions & 0 deletions frontend/src/app/recruitment/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
'use client'

import { useInfiniteQuery } from '@tanstack/react-query'
import Image from 'next/image'
import { useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
import { useRecoilValue } from 'recoil'
import MultipleFilter from '../../components/recruitment/main/MultipleFilter'
import { PositionType } from '../../utils/types'
import {
multiplefilterState,
keywordfilterState,
recruitfilterState,
directionState,
} from '../../utils/atoms'
import Filter from '../../components/recruitment/main/Filter'
import RecruitmentCard from '../../components/recruitment/main/RecruitmentCard'
import WriteIcon from '../../../public/images/svg/pencil-square.svg'

export type RecruitmentDataType = {
id: number
nickname: string
title: string
position: PositionType
techTags: string[]
recruiting: boolean
}

export default function Recruitment() {
const multipleFilter = useRecoilValue(multiplefilterState)
const keywordFilter = useRecoilValue(keywordfilterState)
const recruitFilter = useRecoilValue(recruitfilterState)
const direction = useRecoilValue(directionState)

const [postCount, setPostCount] = useState(0)
const router = useRouter()

function toWrite() {
router.push('/project/write')
}

const getData = async ({ pageParam = 1 }) => {
const params = new URLSearchParams()
params.set('page', String(pageParam))
params.set('size', '12')
params.set('direction', direction)

const positions = multipleFilter
.filter((v) => v.category === 'position')
.map((v) => v.name)
const skills = multipleFilter
.filter((v) => v.category === 'skill')
.map((v) => v.name)

positions.forEach((position) => {
params.append('positions', position)
})

skills.forEach((skill) => {
params.append('tags', skill)
})

if (keywordFilter) {
params.append('keyword', keywordFilter)
}

if (recruitFilter) {
params.append('isRecruiting', String(recruitFilter))
}

const res = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL}/recruitments?${params.toString()}`,
{
headers: {
'Content-Type': 'application/json',
},
},
)

if (!res.ok) {
const data = await res.json()
throw new Error(data.message)
}

const data = await res.json()
setPostCount(data.data.length)
return data.data
}

const { data, fetchNextPage, hasNextPage, isFetchingNextPage, status } =
useInfiniteQuery(
['recruitments', multipleFilter, keywordFilter, recruitFilter, direction],
getData,
{
getNextPageParam: (lastPage, pages) =>
lastPage.length < 12 ? undefined : pages.length + 1,
},
)

useEffect(() => {
if (!isFetchingNextPage) {
const handleScroll = () => {
if (
document.documentElement.scrollHeight -
document.documentElement.scrollTop ===
document.documentElement.clientHeight
) {
fetchNextPage()
}
}
window.addEventListener('scroll', handleScroll)
}
}, [fetchNextPage, isFetchingNextPage, multipleFilter])

if (status === 'loading') {
return <span>Loading...</span>
}

if (status === 'error') {
return <span>Error fetching data</span>
}

return (
<div className="relative flex flex-col items-center w-screen h-auto min-h-screen pb-12 pt-28 ">
<MultipleFilter />
{/* 프로젝트 공유 버튼 */}
<button
className="fixed z-10 flex flex-row items-center px-4 py-1 pt-3 pb-3 my-auto mb-2 font-semibold rounded-full bottom-10 right-10 shrink-0 bg-graphyblue text-slate-50 drop-shadow-md sm:invisible"
onClick={() => toWrite()}
aria-label="toWritePage"
type="button"
>
<Image
className="w-5 h-5 mr-2"
src={WriteIcon}
alt="WriteIcon"
quality={50}
/>
<span className="font-semibold shrink-0">프로젝트 공유</span>
</button>
<Filter postCount={postCount} />
{data.pages.map((group, i) => (
<div
className="relative flex flex-wrap justify-center mx-8"
key={group[i]?.id}
>
{group.map((item: RecruitmentDataType) => (
<div className="mx-8" key={item.id}>
<RecruitmentCard item={item} />
</div>
))}
</div>
))}
{hasNextPage && isFetchingNextPage && <span>Loading more...</span>}
</div>
)
}
6 changes: 5 additions & 1 deletion frontend/src/components/general/ProjectNavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import ProfileIcon from '../../../public/images/svg/profileIcon.svg'
import SearchIcon from '../../../public/images/png/searchIcon.png'
import { searchTextState, usernameState } from '../../utils/atoms'

export default function NavBar({ children }: { children: React.ReactNode }) {
export default function ProjectNavBar({
children,
}: {
children: React.ReactNode
}) {
const accessToken =
typeof window !== 'undefined' ? sessionStorage.getItem('accessToken') : null
const persistToken =
Expand Down
132 changes: 131 additions & 1 deletion frontend/src/components/general/RecruitmentNavBar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,137 @@
'use client'

import { useState, useEffect, useCallback } from 'react'
import { useRouter, usePathname } from 'next/navigation'
import { useRecoilState } from 'recoil'
import Image from 'next/image'
import { PiPaperPlaneTilt } from 'react-icons/pi'
import { TfiBell } from 'react-icons/tfi'

import WriteIcon from '../../../public/images/svg/pencil-square.svg'
import ProfileIcon from '../../../public/images/svg/profileIcon.svg'
import { usernameState } from '../../utils/atoms'
import NoticeModal from '../recruitment/noticification/NoticeModal'

export default function RecruitmentNavBar({
children,
}: {
children: React.ReactNode
}) {
return <div>{children}</div>
const accessToken =
typeof window !== 'undefined' ? sessionStorage.getItem('accessToken') : null
const persistToken =
typeof window !== 'undefined' ? localStorage.getItem('persistToken') : null
const [username, setUsername] = useRecoilState(usernameState)
const [btnText, setBtnText] = useState<string>('로그인')

const [isOpenModal, setOpenModal] = useState<boolean>(false)

const router = useRouter()
const pathname = usePathname()

const onClickToggleModal = useCallback(() => {
setOpenModal(!isOpenModal)
}, [isOpenModal])

function signOut() {
setUsername('')
if (accessToken) {
sessionStorage.removeItem('accessToken')
} else {
localStorage.removeItem('persistToken')
}
}

const handleLogin = () => {
if (accessToken || persistToken) {
signOut()
setBtnText('로그인')
} else {
router.push('/project/login')
}
}

useEffect(() => {
if (accessToken || persistToken) {
setBtnText('로그아웃')
} else {
setBtnText('로그인')
}
}, [pathname])

if (pathname === '/project/login' || pathname === '/project/registration') {
return children
}

return (
<div>
<div className="fixed z-20 mb-5 w-screen flex justify-between content-center overflow-hidden border-b border-zinc-400 bg-white pt-3 pb-3 px-8 align-middle font-ng-eb">
{/* 로고 */}
<button
onClick={() => router.push('/')}
className="hidden font-lato-b text-4xl text-graphyblue sm:block"
type="button"
>
Graphy
</button>
<button
onClick={() => router.push('/')}
className="font-lato-b text-4xl text-graphyblue sm:hidden"
type="button"
>
G
</button>
<div className="flex">
<button
className=" mr-4 whitespace-nowrap rounded-full bg-graphyblue px-4 text-white"
type="button"
onClick={() => handleLogin()}
>
{btnText}
</button>

{/* 프로젝트 작성 버튼 */}
<button
className="invisible mx-auto mr-4 flex h-0 w-0 shrink-0 flex-row flex-nowrap items-center rounded-full bg-graphyblue text-white sm:visible sm:mr-5
sm:h-auto sm:w-auto sm:px-4 sm:py-1"
onClick={() => router.push('/project/new-post')}
aria-label="toWritePage"
type="button"
>
<Image className="mr-2 h-5 w-5" src={WriteIcon} alt="WriteIcon" />
<span className="font-semibold">프로젝트 공유</span>
</button>

<div className="flex h-full items-between relative gap-4">
<button type="button" className="text-[10px] font-light">
<PiPaperPlaneTilt size="28" />
</button>
<button
onClick={onClickToggleModal}
type="button"
className="text-[10px] font-light"
>
<TfiBell size="28" />
</button>
{/* 마이페이지 아이콘 */}
<button
style={{ display: btnText === '로그인' ? 'none' : 'block' }}
type="button"
onClick={() => router.push(`/project/profile/${username}`)}
>
<Image
className="h-8 w-8 appearance-none"
src={ProfileIcon}
alt="ProfileIcon"
/>
</button>
</div>
</div>
</div>
{isOpenModal ? (
<NoticeModal onClickToggleModal={onClickToggleModal} />
) : null}
{children}
</div>
)
}
Loading

0 comments on commit 01a421e

Please sign in to comment.