Skip to content

Commit

Permalink
[feat/#46] 초대하기 기능 추가, 내 그룹 여부에 따라 크루 생성, 가입 제한
Browse files Browse the repository at this point in the history
  • Loading branch information
lkhoony committed Sep 17, 2024
1 parent aa2c4e2 commit fffa9fc
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 70 deletions.
154 changes: 98 additions & 56 deletions src/components/Crew/CrewList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,95 +6,123 @@ import { useModals } from "@/hooks/useModals"
import useMyGroup from "@/hooks/useMyGroup"
import CreateCrewIcon from "@assets/icons/crew-create-button-icon.svg?react"
import SortCrewIcon from "@assets/icons/crew-sort-icon.svg?react"
import { ReactElement, useEffect, useRef, useState } from "react"
import { ReactElement, useCallback, useEffect, useRef, useState, useMemo } from "react"
import { modals } from "../Modal/Modals"
import MyCrewRankingContainer from "./MyCrew/MyCrewRankingContainer"
import { useNavigate, useSearchParams } from "react-router-dom"
import RoutePath from "@/constants/routes.json"

const SORT_LIST = [
{ sort: "userCount,desc", label: "크루원 많은 순" },
{ sort: "createdAt,desc", label: "최신 생성 크루 순" },
]

const CrewList = (): ReactElement => {
const { myGroupData, ranks, myRank } = useMyGroup()
const navigate = useNavigate()
const { myGroupData, ranks, myRank, refetchAll } = useMyGroup()
const [isDropdownOpen, setIsDropdownOpen] = useState<boolean>(false)
console.log("myGroupData: ", myGroupData)
const [searchParams, setSearchParams] = useSearchParams()

const [params, setParams] = useState<groupsReq>({
page: 0,
size: 10000,
sort: "userCount,desc",
})

const { data, isLoading, isError, refetch } = useGetGroups(params)

const { openModal } = useModals()

const openCreateModal = (): void => {
openModal(modals.createCrewModal, {
onSubmit: () => {
refetch()
},
})
}

const openJoinCrewModal = (id: number): void => {
openModal(modals.joinCrewModal, {
id,
onSubmit: () => {
console.log("open")
},
})
}
// openCreateModal 메모이제이션
const openCreateModal = useCallback((): void => {
if (myGroupData) {
openModal(modals.ToWithdrawModal, {
onSubmit: () => {
navigate(RoutePath.MYCREW)
},
})
} else {
openModal(modals.createCrewModal, {
onSubmit: () => {
refetch()
refetchAll()
},
})
}
}, [myGroupData, navigate, openModal, refetch, refetchAll])

// openJoinCrewModal 메모이제이션
const openJoinCrewModal = useCallback(
(id: number | undefined): void => {
openModal(modals.joinCrewModal, {
id,
onSubmit: () => {
console.log("open")
},
})
},
[openModal]
)

const openInviteModal = (): void => {
// openInviteModal 메모이제이션
const openInviteModal = useCallback((): void => {
openModal(modals.inviteCrewModal, {
id: Number(myGroupData?.id),
onSubmit: () => {
console.log("open")
},
})
}

const dropdownRef = useRef<HTMLDivElement>(null)
}, [myGroupData, openModal])

// toggleDropdown 함수
const toggleDropdown = (): void => {
setIsDropdownOpen((prev) => !prev)
}

const createSortList = (): JSX.Element[] => {
// createSortList 메모이제이션
const createSortList = useMemo(() => {
return SORT_LIST.map((s) => (
// eslint-disable-next-line max-len
<div
key={`sort-list-${s.sort}`}
className="cursor-pointer text-[13px] font-medium leading-[24px] text-zinc-400"
onClick={() => {
setParams({ ...params, sort: s.sort as sort })
setParams((prev) => ({ ...prev, sort: s.sort as sort }))
setIsDropdownOpen(false)
}}
>
{s.label}
</div>
))
}

const createGroupList = (_groups: group[] | undefined): JSX.Element | null => {
if (!_groups) return null
}, [])

// createGroupList 메모이제이션
const createGroupList = useCallback(
(_groups: group[] | undefined): JSX.Element | null => {
if (!_groups) return null

if (_groups.length === 0) {
return (
<div className="flex flex-grow flex-col items-center justify-center">
<img src={EmptyCrewImage} alt="empty crew" />
<div className="text-center text-[14px] font-semibold leading-[22px]">
{"만들어진 크루가 아직 없습니다."}
</div>
</div>
)
}

if (_groups.length === 0) {
return (
<div className="flex flex-grow flex-col items-center justify-center">
<img src={EmptyCrewImage} alt="empty crew" />
<div className="text-center text-[14px] font-semibold leading-[22px]">{"만들어진 크루가 아직 없습니다."}</div>
<div className="flex flex-grow flex-col gap-[8px]">
{_groups.map((g) => (
<CrewItem key={`crew-item-${g.id}`} group={g} onClickDetail={() => openJoinCrewModal(g.id)} />
))}
</div>
)
}
return (
<div className="flex flex-grow flex-col gap-[8px]">
{_groups.map((g) => (
<CrewItem key={`crew-item-${g.id}`} group={g} onClickDetail={() => openJoinCrewModal(g.id)} />
))}
</div>
)
}
},
[openJoinCrewModal]
)

// Dropdown 외부 클릭 감지 메모이제이션
const dropdownRef = useRef<HTMLDivElement>(null)

useEffect(() => {
const handleClickOutside = (event: MouseEvent): void => {
Expand All @@ -107,7 +135,21 @@ const CrewList = (): ReactElement => {
return () => {
document.removeEventListener("mousedown", handleClickOutside)
}
}, [dropdownRef])
}, [])

// URL에서 groupId 추출 후 모달 열기
useEffect(() => {
const groupId = searchParams.get("groupId")

if (groupId) {
openJoinCrewModal(Number(groupId))
const removeGroupIdFromUrl = (): void => {
searchParams.delete("groupId")
setSearchParams(searchParams)
}
removeGroupIdFromUrl()
}
}, [openJoinCrewModal, searchParams, setSearchParams])

return (
<div className="flex h-full w-full flex-col">
Expand All @@ -120,22 +162,22 @@ const CrewList = (): ReactElement => {
openInviteModal={openInviteModal}
/>
)}

{/* header */}
<div className="mb-[24px] flex w-full items-center">
<div className="flex-grow text-[22px] font-bold text-zinc-900">
<span>전체크루</span>
<span>{isLoading ? "" : `(${data?.totalCount})`}</span>
</div>
{!myGroupData ||
(myGroupData && Object.keys(myGroupData).length === 0 && (
<div
className="flex w-[138px] cursor-pointer items-center justify-center gap-[10px] rounded-[33px] bg-zinc-800 p-[10px] text-sm font-semibold text-white"
onClick={openCreateModal}
>
<CreateCrewIcon />
<div>크루 만들기</div>
</div>
))}
{(!myGroupData || (myGroupData && Object.keys(myGroupData).length === 0)) && (
<div
className="flex w-[138px] cursor-pointer items-center justify-center gap-[10px] rounded-[33px] bg-zinc-800 p-[10px] text-sm font-semibold text-white"
onClick={openCreateModal}
>
<CreateCrewIcon />
<div>크루 만들기</div>
</div>
)}
</div>

{/* sort */}
Expand All @@ -153,7 +195,7 @@ const CrewList = (): ReactElement => {
: "pointer-events-none -translate-y-2 scale-95 opacity-0"
}`}
>
{createSortList()}
{createSortList}
</div>
</div>

Expand Down
85 changes: 85 additions & 0 deletions src/components/Crew/CrewRanking.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Crew1stCrownIcon from "@assets/icons/crew-1st-crown.svg?react"

const RankPillar = ({ rank, name, score, height }: any) => {
const rankStyleMap: { gap: string; bgColor: string; fontSize: string }[] = [
{
gap: "6",
bgColor: "#8BBAFE",
fontSize: "32px",
},
{
gap: "6",
bgColor: "#DCEBFD",
fontSize: "22px",
},
{
gap: "4",
bgColor: "#DCEBFD",
fontSize: "22px",
},
]

const style = rankStyleMap[rank - 1]

return (
<div className="flex flex-col items-center text-zinc-800" style={{ height }}>
<div
className={`flex w-[180px] flex-grow flex-col items-center justify-end py-6 gap-${style.gap} rounded-[12px]`}
style={{
minHeight: "100px",
backgroundColor: style.bgColor,
}}
>
<div className="flex flex-col items-center">
{rank === 1 && <Crew1stCrownIcon className="mb-2 h-6 w-6" />}
<div className={`font-medium text-[${rankStyleMap[rank - 1].fontSize}]`}>{rank}</div>
</div>
<div className="text-xl font-semibold">{name}</div>
<div className="text-[15px] font-normal ">틀어짐 {score}</div>
</div>
</div>
)
}

const RankCard = ({ rank, name, score, isMe }: any) => (
<div
className={`flex items-center justify-between py-1 pl-3 pr-4 ${isMe ? "rounded-full bg-[#DCEBFD]" : ""} h-[40px]`}
>
<div className="flex items-center gap-4">
<div className="flex h-8 w-8 items-center justify-center font-normal text-zinc-900">{rank}</div>
<span className={isMe ? "font-medium text-[#1A75FF]" : ""}>{isMe ? "나" : name}</span>
</div>
<span className="text-[13px] font-normal text-zinc-400">자세경고 {score}</span>
</div>
)

const CrewRanking = ({ rankings, myRank }: { rankings: any[]; myRank: any }) => {
const topThree = rankings.slice(0, 3)

return (
<div className="flex h-full gap-12">
{/* 1, 2, 3등 랭킹 */}
<div className="flex h-full flex-1 items-end gap-3">
<RankPillar rank={1} name={topThree[0].name} score={topThree[0].score} height="100%" />
<RankPillar rank={2} name={topThree[1].name} score={topThree[1].score} height="82%" />
<RankPillar rank={3} name={topThree[2].name} score={topThree[2].score} height="72%" />
</div>

{/* 전체 랭킹 목록 */}
<div className="flex flex-1 flex-col overflow-hidden">
<div className="flex h-full flex-col overflow-hidden bg-white">
<div className="flex-shrink-0">
<RankCard rank={myRank.rank} name={myRank.name} score={myRank.score} isMe={true} />
</div>
<div className="scrollbar-hide flex-grow overflow-y-auto">
{rankings.map((rank, index) => (
<RankCard key={index} rank={rank.rank} name={rank.name} score={rank.score} isMe={false} />
))}
</div>
</div>
</div>
</div>
)
}

export default CrewRanking
6 changes: 3 additions & 3 deletions src/components/Crew/MyCrew/CrewRanking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ const CrewRanking = ({ rankings, myRank }: { rankings: any[]; myRank: any }) =>
<div className="flex h-full gap-12">
{/* 1, 2, 3등 랭킹 */}
<div className="flex h-full flex-1 items-end gap-3">
<RankPillar rank={1} name={topThree[0].name} score={topThree[0].score} height="100%" />
<RankPillar rank={2} name={topThree[1].name} score={topThree[1].score} height="82%" />
<RankPillar rank={3} name={topThree[2].name} score={topThree[2].score} height="72%" />
{topThree[0] && <RankPillar rank={1} name={topThree[0].name} score={topThree[0].score} height="100%" />}
{topThree[1] && <RankPillar rank={2} name={topThree[1].name} score={topThree[1].score} height="82%" />}
{topThree[2] && <RankPillar rank={3} name={topThree[2].name} score={topThree[2].score} height="72%" />}
</div>

{/* 전체 랭킹 목록 */}
Expand Down
29 changes: 24 additions & 5 deletions src/components/Modal/InviteCrewModal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
import { ModalProps } from "@/contexts/ModalsContext"
import ModalContainer from "@components/ModalContainer"
import { useState } from "react"
import RoutePath from "@/constants/routes.json"

const InviteCrewModal = (props: ModalProps): React.ReactElement => {
const { onClose, onSubmit } = props
const InviteCrewModal = (props: ModalProps & { id: string }): React.ReactElement => {
const { onClose, id } = props
const [isCopied, setIsCopied] = useState(false)

// 현재 URL을 기반으로 초대 링크 생성
const currentUrl = `${window.location.protocol}//${window.location.hostname}${
window.location.port ? `:${window.location.port}` : ""
}${RoutePath.CREW}?groupId=${id}`

const handleCopy = (): void => {
navigator.clipboard
.writeText(currentUrl)
.then(() => {
setIsCopied(true)
})
.catch((err) => {
console.error("Failed to copy: ", err)
})
}

return (
<ModalContainer onClose={onClose}>
Expand All @@ -16,17 +35,17 @@ const InviteCrewModal = (props: ModalProps): React.ReactElement => {
<div className="text-center text-[15px] font-normal text-zinc-900">
아래 초대 링크를 복사해 크루에 초대해 보세요.
</div>
<div className={"w-full rounded-xl border border-gray-200 p-3 font-normal"}>{"https://alignlab.site/"}</div>
<div className={"w-full rounded-xl border border-gray-200 p-3 font-normal"}>{currentUrl}</div>
</div>

{/* button */}
<button
className="w-[256px] rounded-[40px] bg-[#1A75FF] px-10 py-3 text-base font-semibold text-white"
onClick={onSubmit}
onClick={handleCopy}
>
초대 링크 복사하기
</button>
<div className="mt-3 text-sm font-medium text-[#1A75FF]">초대 링크가 복사되었어요.</div>
{isCopied && <div className="mt-3 text-sm font-medium text-[#1A75FF]">초대 링크가 복사되었어요.</div>}
</div>
</ModalContainer>
)
Expand Down
Loading

0 comments on commit fffa9fc

Please sign in to comment.