Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FE/#290 프로젝트 구인 목록 페이지 퍼블리싱 및 알림기능 구현 #305

Merged
merged 14 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading