Skip to content

Commit

Permalink
feat : 알림 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
Yujin-Baek committed Nov 25, 2023
1 parent 02e17fc commit 8d5badd
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 80 deletions.
31 changes: 28 additions & 3 deletions frontend/src/app/recruitment/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
'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/MultipleFilter'
import MultipleFilter from '../../components/recruitment/main/MultipleFilter'
import { PositionType } from '../../utils/types'
import { filterState } from '../../utils/atoms'
import Filter from '../../components/recruitment/Filter'
import RecruitmentCard from '../../components/recruitment/RecruitmentCard'
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
Expand All @@ -21,6 +24,11 @@ export type RecruitmentDataType = {
export default function Recruitment() {
const filter = useRecoilValue(filterState)
const [postCount, setPostCount] = useState(0)
const router = useRouter()

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

const getData = async ({ pageParam = 1 }) => {
const params = new URLSearchParams()
Expand Down Expand Up @@ -105,6 +113,23 @@ export default function Recruitment() {
return (
<div className="relative h-auto min-h-screen w-screen pt-28 pb-12 flex flex-col items-center ">
<MultipleFilter />
{/* 프로젝트 공유 버튼 */}
<button
className="fixed bottom-10 right-10 z-10 my-auto mb-2 flex shrink-0 flex-row items-center rounded-full
bg-graphyblue px-4 py-1 pt-3 pb-3 font-semibold text-slate-50 drop-shadow-md
sm:invisible"
onClick={() => toWrite()}
aria-label="toWritePage"
type="button"
>
<Image
className="mr-2 h-5 w-5"
src={WriteIcon}
alt="WriteIcon"
quality={50}
/>
<span className="shrink-0 font-semibold">프로젝트 공유</span>
</button>
<Filter postCount={postCount} />
{data.pages.map((group, i) => (
<div
Expand Down
128 changes: 56 additions & 72 deletions frontend/src/components/general/RecruitmentNavBar.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
'use client'

import { ChangeEvent, useState, useEffect } from 'react'
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 SearchIcon from '../../../public/images/png/searchIcon.png'
import { searchTextState, usernameState } from '../../utils/atoms'
import { usernameState } from '../../utils/atoms'
import NoticeModal from '../recruitment/noticification/NoticeModal'

export default function RecruitmentNavBar({
children,
Expand All @@ -19,16 +21,17 @@ export default function RecruitmentNavBar({
typeof window !== 'undefined' ? sessionStorage.getItem('accessToken') : null
const persistToken =
typeof window !== 'undefined' ? localStorage.getItem('persistToken') : null
const [searchText, SetSearchText] = useRecoilState(searchTextState)
const [username, setUsername] = useRecoilState(usernameState)
const [btnText, setBtnText] = useState<string>('로그인')

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

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

const getSearchData = (e: ChangeEvent<HTMLInputElement>) => {
SetSearchText(e.target.value)
}
const onClickToggleModal = useCallback(() => {
setOpenModal(!isOpenModal)
}, [isOpenModal])

function signOut() {
setUsername('')
Expand All @@ -39,17 +42,6 @@ export default function RecruitmentNavBar({
}
}

const handleSearch = () => {
if (searchText === '' || searchText === '@') {
router.push('/')
} else if (searchText.charAt(0) === '@') {
const searchUserName = searchText.substring(1)
router.push(`/project/search-user/${searchUserName}`)
} else {
router.push(`/project/search-post/${searchText}`)
}
}

const handleLogin = () => {
if (accessToken || persistToken) {
signOut()
Expand All @@ -73,80 +65,72 @@ export default function RecruitmentNavBar({

return (
<div>
<div className="fixed z-20 mb-5 flex w-screen flex-row content-center overflow-hidden border-b border-zinc-400 bg-white pt-3 pb-3 align-middle font-ng-eb">
<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="ml-8 hidden font-lato-b text-4xl text-graphyblue sm:block"
className="hidden font-lato-b text-4xl text-graphyblue sm:block"
type="button"
>
Graphy
</button>
<button
onClick={() => router.push('/')}
className="ml-8 font-lato-b text-4xl text-graphyblue sm:hidden"
className="font-lato-b text-4xl text-graphyblue sm:hidden"
type="button"
>
G
</button>

{/* 검색창 */}
<div className="relative mx-4 flex h-auto w-full items-center rounded-xl border">
<input
value={searchText}
onChange={getSearchData}
type="text"
alt="search"
placeholder="search"
className="h-auto w-full appearance-none pl-2 outline-none"
onKeyPress={(e) => {
if (e.key === 'Enter') {
handleSearch()
}
}}
/>
<div className="flex">
<button
className="mr-2"
onClick={handleSearch}
aria-label="SearchButton"
className=" mr-4 whitespace-nowrap rounded-full bg-graphyblue px-4 text-white"
type="button"
onClick={() => handleLogin()}
>
<Image className="h-6 w-auto" src={SearchIcon} alt="SearchIcon" />
{btnText}
</button>
</div>
<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
{/* 프로젝트 작성 버튼 */}
<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>
{/* 마이페이지 아이콘 */}
<button
className="mr-12"
style={{ display: btnText === '로그인' ? 'none' : 'block' }}
type="button"
onClick={() => router.push(`/project/profile/${username}`)}
>
<Image
className="fixed top-4 right-4 h-8 w-8 appearance-none"
src={ProfileIcon}
alt="ProfileIcon"
/>
</button>
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>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { useEffect, useState } from 'react'
import { useRecoilState } from 'recoil'
import Select, { StylesConfig } from 'react-select'
import Image from 'next/image'
import { filterState } from '../../utils/atoms'
import x from '../../../public/images/svg/tag_x.svg'
import { filterState } from '../../../utils/atoms'
import x from '../../../../public/images/svg/tag_x.svg'
import {
Position,
Skill,
PositionType,
SkillType,
FilterType,
} from '../../utils/types'
} from '../../../utils/types'

const positionOptions = Object.values(Position).map((position) => ({
value: position,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HiOutlineUserCircle } from 'react-icons/hi2'
import { TfiComment } from 'react-icons/tfi'
import { PositionType } from '../../utils/types'
import { PositionType } from '../../../utils/types'

type RecruitmentCardProps = {
item: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { filterState } from '@/utils/atoms'
import { useEffect, useState } from 'react'
import { useRecoilState } from 'recoil'
import '../../../public/css/toggle-button.css'
import '../../../../public/css/toggle-button.css'

export default function ToggleButton() {
const [filter, setFilter] = useRecoilState(filterState)
Expand Down
71 changes: 71 additions & 0 deletions frontend/src/components/recruitment/noticification/NoticeModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, { PropsWithChildren, useEffect, useState } from 'react'
import Noticification from './Noticification'

type PropsType = {
onClickToggleModal: () => void
}

type NotificationDataType = {
id: number
type: string
content: string
read: boolean
}

export default function NoticeModal({
onClickToggleModal,
}: PropsWithChildren<PropsType>) {
const accessToken =
typeof window !== 'undefined' ? sessionStorage.getItem('accessToken') : null
const persistToken =
typeof window !== 'undefined' ? localStorage.getItem('persistToken') : null

const [data, setData] = useState<NotificationDataType[]>([])

const getData = async ({ pageParam = 1 }) => {
const params = new URLSearchParams()
params.set('page', String(pageParam))
params.set('size', '12')
params.set('direction', 'ASC')
const res = await fetch(
`${process.env.NEXT_PUBLIC_BASE_URL}/notifications?${params.toString()}`,
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken || persistToken}`,
},
},
)

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

const resData = await res.json()
setData(resData.data)
}

useEffect(() => {
getData({ pageParam: 1 })
}, [])

return (
<div className="h-full w-full relative">
<div className="overflow-auto fixed z-50 w-[300px] h-[350px] rounded-[15px] bg-white shadow-md top-[70px] right-0">
{data.map((item: NotificationDataType) => (
<Noticification item={item} />
))}
</div>
<button
aria-label="Toggle modal"
className="fixed top-0 left-0 right-0 bottom-0 z-40 h-full w-screen"
onClick={(e: React.MouseEvent) => {
e.preventDefault()
onClickToggleModal?.()
}}
type="button"
/>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { HiOutlineUserCircle } from 'react-icons/hi2'

type NotificationType = {
item: {
id: number
type: string
content: string
read: boolean
}
}

export default function RecruitmentCard({ item }: NotificationType) {
return (
<div className="border-b-2 border-gray-200 flex items-center w-[290px] h-[50px] text-lightgray text-[11px] py-2 px-4 bg-white border-solid">
<div>
<HiOutlineUserCircle strokeWidth="1" size="30" />
</div>
<span className="ml-2">{item.content}</span>
</div>
)
}

0 comments on commit 8d5badd

Please sign in to comment.