diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.tsx index 8d35693..a201fcf 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/Card.tsx @@ -4,14 +4,16 @@ import { Cards } from '@/types/dashboardView'; import { useModal } from '@/app/(with-header-sidebar)/mydashboard/_hooks/useModal'; import Modal from '@/app/(with-header-sidebar)/mydashboard/_components/modal/Modal'; import CardInfo from './card-detail/CardInfo'; +import HeaderMenu from './card-detail/HeaderMenu'; import styles from './Card.module.css'; interface Props { item: Cards; index: number; + columnTitle: string; } -function Card({ item, index }: Props) { +function Card({ item, index, columnTitle }: Props) { const { isOpen, openModal, isClosing, closeModal } = useModal(); if (!item || !item.id) { @@ -39,8 +41,11 @@ function Card({ item, index }: Props) { onClose={closeModal} title={item.title} hasCloseButton={true} + headerComponent={() => ( + + )} > - + )} diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.tsx index 6d62384..71962c3 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/Column.tsx @@ -99,7 +99,6 @@ function Column({ /> -
{items.map((item, index) => - item ? : null + item ? ( + + ) : null )} - {provided.placeholder} -
)} diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.module.css b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.module.css index 0ed9468..c0024b6 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.module.css +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.module.css @@ -17,6 +17,7 @@ display: flex; flex-direction: column; justify-content: flex-start; + gap: 2px; } .label { @@ -26,13 +27,14 @@ line-height: 20px; } -.description { +.description, +.avatarContainer { color: var(--black-100); font-size: 12px; font-weight: 400; } -.assignee .description { +.avatarContainer { display: flex; align-items: center; gap: 8px; @@ -43,7 +45,7 @@ height: 26px; } -.dueDate .description { +.description { margin-top: 8px; } @@ -57,6 +59,10 @@ gap: 16px; } + .assignee { + gap: 5px; + } + .avatar { width: 34px; height: 34px; diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.tsx index 309ff37..b722abf 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.tsx +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/Assignment.tsx @@ -10,11 +10,6 @@ interface AssignmentProps { export default function Assignment({ card }: AssignmentProps) { if (!card) return null; - // const { assignee, dueDate } = { - // dueDate: '2024-11-30 12:00', - // assignee: { profileImageUrl: null, nickname: 'manta', id: 1 }, - // }; - const { assignee, dueDate } = card; return ( @@ -23,7 +18,7 @@ export default function Assignment({ card }: AssignmentProps) { {assignee && (
담당자
-
+
-
카드 상세내용 + 댓글 영역
+
+
+
+ +
+ +
+ {tags.map((tag, index) => ( + + ))} +
+
+

{description}

+ {imageUrl && ( +
+ 할일 이미지 +
+ )} +
); } diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.module.css b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.module.css new file mode 100644 index 0000000..36db845 --- /dev/null +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.module.css @@ -0,0 +1,18 @@ +.headerMenu { + display: flex; + align-items: center; + margin-right: 16px; + position: relative; +} + +.moreButton { + display: inline-flex; + align-items: center; +} + +.menuContainer { + position: absolute; + z-index: 999; + top: 40px; + right: 0; +} diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.tsx new file mode 100644 index 0000000..1187440 --- /dev/null +++ b/src/app/(with-header-sidebar)/dashboard/[id]/components/card-detail/HeaderMenu.tsx @@ -0,0 +1,43 @@ +import More from '@/components/svg/More'; +import type { Menu } from '@/types/menu'; +import MenuDropdown from '@/components/MenuDropdown'; +import { useMenu } from '@/hooks/useMenu'; +import styles from './HeaderMenu.module.css'; +import { deleteCard } from '@/lib/cardService'; +import { useRouter } from 'next/navigation'; +import useDashboardStore from '@/store/dashboardStore'; + +interface HeaderMenuProps { + cardId: number; + closeModal: () => void; +} + +export default function HeaderMenu({ cardId, closeModal }: HeaderMenuProps) { + const router = useRouter(); + const { dashboard } = useDashboardStore(); + const { isMenuVisible, toggleMenu } = useMenu(); + + const handelDeleteClick = async () => { + await deleteCard(cardId); + closeModal(); + router.replace(`/dashboard/${dashboard?.id}`); + }; + + const cardMenus: Menu[] = [ + { name: '수정하기', handleOnClick: () => console.log('수정하기 클릭') }, + { name: '삭제하기', handleOnClick: handelDeleteClick }, + ]; + + return ( +
+ + {isMenuVisible && ( +
+ +
+ )} +
+ ); +} diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useMember.ts b/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useMember.ts index 1f4c3a3..a06fbd3 100644 --- a/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useMember.ts +++ b/src/app/(with-header-sidebar)/dashboard/[id]/edit/_hooks/useMember.ts @@ -15,7 +15,7 @@ const DEFAULT_MEMBERS_STATE: MemberState = { members: [], }; -const useMember = (dashboardId: string, pageSize = 4) => { +const useMember = (dashboardId: string | null, pageSize = 4) => { const [memberState, setMemberState] = useState( DEFAULT_MEMBERS_STATE ); diff --git a/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.module.css b/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.module.css index a87c115..fad776c 100644 --- a/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.module.css +++ b/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.module.css @@ -44,15 +44,15 @@ color: var(--black-100); } -.close { +.closeButtonWrapper { + position: relative; width: 24px; height: 24px; - transition: 200ms; } -.close:hover { - filter: brightness(2); - transform: rotate(90deg); +.closeButton { + display: inline-flex; + align-items: center; } @keyframes fadeIn { @@ -107,4 +107,9 @@ flex: 1; color: var(--black-100); } + + .closeButtonWrapper { + width: 32px; + height: 32px; + } } diff --git a/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.tsx b/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.tsx index 5787627..f3040c8 100644 --- a/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.tsx +++ b/src/app/(with-header-sidebar)/mydashboard/_components/modal/Modal.tsx @@ -12,7 +12,7 @@ interface ModalProps { allowDimClose?: boolean; title?: string; hasCloseButton?: boolean; - headerComponent?: React.ComponentType; + headerComponent?: React.ComponentType; children: ReactNode; } @@ -59,18 +59,16 @@ export default function Modal({ {title &&

{title}

} {Component && } {hasCloseButton && ( - +
+ +
)}
{children} diff --git a/src/app/(with-header-sidebar)/mydashboard/_hooks/useApi.ts b/src/app/(with-header-sidebar)/mydashboard/_hooks/useApi.ts index b6f92ee..c01a32e 100644 --- a/src/app/(with-header-sidebar)/mydashboard/_hooks/useApi.ts +++ b/src/app/(with-header-sidebar)/mydashboard/_hooks/useApi.ts @@ -31,7 +31,6 @@ export default function useApi< const [error, setError] = useState(null); const fetchData = useCallback(async () => { - // TODO if (loading) return; add? setIsLoading(true); setError(null); diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 7f17ee2..34b73b6 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -1,5 +1,6 @@ import Image from 'next/image'; import styles from './Avatar.module.css'; +import { getRandomColor } from '@/utils/colorUtils'; interface AvatarProps { name: string | number; @@ -36,20 +37,3 @@ export default function Avatar({ ); } - -const getRandomColor = (name: string) => { - const hash = [...name].reduce( - (acc, char) => acc + char.charCodeAt(0) * 31, - 0 - ); - const getValue = (offset: number) => { - const baseValue = (((hash >> offset) % 0xff) % 76) + 180; - const maxValue = 225; - return Math.min(baseValue, maxValue); - }; - const red = getValue(0); - const green = getValue(8); - const blue = getValue(16); - - return `rgb(${red}, ${green}, ${blue})`; -}; diff --git a/src/components/MenuDropdown.module.css b/src/components/MenuDropdown.module.css new file mode 100644 index 0000000..d803ccc --- /dev/null +++ b/src/components/MenuDropdown.module.css @@ -0,0 +1,40 @@ +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.menuDropdown { + border-radius: 6px; + border: 1px solid var(--gray-300); + background: var(--white); + box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.08); + padding: 6px; + display: flex; + flex-direction: column; + gap: 5px; + animation: slideDown 0.3s ease-out; +} + +.button { + border-radius: 4px; + color: var(--black); + font-size: 14px; + font-weight: 400; + transition: + background-color 0.3s ease, + color 0.3s ease, + transform 0.3s ease; + white-space: nowrap; + padding: 5px 16px; +} + +.button:hover { + background-color: var(--violet-light); + color: var(--violet); +} diff --git a/src/components/MenuDropdown.tsx b/src/components/MenuDropdown.tsx new file mode 100644 index 0000000..6ef0688 --- /dev/null +++ b/src/components/MenuDropdown.tsx @@ -0,0 +1,23 @@ +import type { Menu } from '@/types/menu'; +import styles from './MenuDropdown.module.css'; + +interface MenuDropdownProps { + menus: Menu[]; +} + +export default function MenuDropdown({ menus }: MenuDropdownProps) { + return ( +
+ {menus.map((menu, index) => ( + + ))} +
+ ); +} diff --git a/src/components/card/ColumnLabel.module.css b/src/components/card/ColumnLabel.module.css new file mode 100644 index 0000000..d3b51b3 --- /dev/null +++ b/src/components/card/ColumnLabel.module.css @@ -0,0 +1,11 @@ +.columnLabel { + border-radius: 16px; + background: var(--violet-light); + color: var(--violet); + font-size: 12px; + font-weight: 400; + line-height: 18px; + display: flex; + gap: 6px; + padding: 4px 10px; +} diff --git a/src/components/card/ColumnLabel.tsx b/src/components/card/ColumnLabel.tsx new file mode 100644 index 0000000..54ddede --- /dev/null +++ b/src/components/card/ColumnLabel.tsx @@ -0,0 +1,17 @@ +import styles from './ColumnLabel.module.css'; +import Dot from '../svg/Dot'; + +interface ColumnLabelProps { + name: string; +} + +export default function ColumnLabel({ name }: ColumnLabelProps) { + return ( +
+
+ +
+ {name} +
+ ); +} diff --git a/src/components/card/Tag.module.css b/src/components/card/Tag.module.css new file mode 100644 index 0000000..d46a9d9 --- /dev/null +++ b/src/components/card/Tag.module.css @@ -0,0 +1,7 @@ +.tag { + font-size: 12px; + font-weight: 400; + line-height: 18px; + padding: 2px 6px; + border-radius: 4px; +} diff --git a/src/components/card/Tag.tsx b/src/components/card/Tag.tsx new file mode 100644 index 0000000..d0b1762 --- /dev/null +++ b/src/components/card/Tag.tsx @@ -0,0 +1,20 @@ +import { getDarkerColor, getRandomColor } from '@/utils/colorUtils'; +import styles from './Tag.module.css'; + +interface TagProps { + name: string; +} + +export default function Tag({ name }: TagProps) { + const mainColor = getRandomColor(name); + const style = { + backgroundColor: mainColor, + color: getDarkerColor(mainColor), + }; + + return ( +
+ {name} +
+ ); +} diff --git a/src/components/header/DashboardMembers.tsx b/src/components/header/DashboardMembers.tsx index 4a05b7d..025d8ce 100644 --- a/src/components/header/DashboardMembers.tsx +++ b/src/components/header/DashboardMembers.tsx @@ -16,7 +16,7 @@ export default function DashboardMembers() { const { isMobile } = useWindowSize(); const { members, totalPages } = useMember( - dashboard?.id.toString() || '0', + dashboard?.id.toString() || null, MEMBERS_VIEW_COUNT.desktop ); diff --git a/src/components/header/Header.module.css b/src/components/header/Header.module.css index d791386..717acf7 100644 --- a/src/components/header/Header.module.css +++ b/src/components/header/Header.module.css @@ -44,50 +44,6 @@ display: none; } -.userInfoContainer { - display: flex; - align-items: center; -} - -.userInfoContainer::before { - content: ''; - border: 0.5px solid var(--gray-300); - height: 34px; -} - -.userInfoButton.userInfoButton { - margin-left: 16px; - display: flex; - align-items: center; - background-color: transparent; -} - -.myMenu { - position: absolute; - top: 75px; - right: 10px; - padding: 6px 0; - border: 1px solid var(--gray-300); - border-radius: 6px; - color: var(--gray-500); - font-size: 14px; - font-weight: 500; - display: flex; - flex-direction: column; - gap: 5px; - background: var(--white); -} - -.myMenu div { - padding: 3px 12px; - transition: background-color 0.3s ease; -} - -.myMenu div:hover { - cursor: pointer; - background: var(--violet-light); -} - @media screen and (min-width: 768px) { .header { padding-left: 40px; @@ -111,17 +67,6 @@ .icon { display: block; } - - .userInfoWrapper { - margin-left: 36px; - } - - .myMenu { - min-width: 120px; - text-align: center; - top: 80px; - right: 50px; - } } @media screen and (min-width: 1199px) { diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx index 87b3dca..af68f89 100644 --- a/src/components/header/Header.tsx +++ b/src/components/header/Header.tsx @@ -1,13 +1,12 @@ 'use client'; import { usePathname, useRouter } from 'next/navigation'; -import { useState } from 'react'; import Image from 'next/image'; import Button from '../Button'; -import UserInfo from './UserInfo'; import Title from './Title'; -import styles from './Header.module.css'; import useDashboardStore from '@/store/dashboardStore'; +import UserSection from './UserSection'; +import styles from './Header.module.css'; interface HeaderProps { component?: React.ComponentType; @@ -16,21 +15,11 @@ interface HeaderProps { export default function Header({ component: Component }: HeaderProps) { const router = useRouter(); const dashboard = useDashboardStore((state) => state.dashboard); - const [isMenuVisible, setIsMenuVisible] = useState(false); - - const handleUserInfoClick = () => { - setIsMenuVisible(!isMenuVisible); - }; const handleSettingsClick = () => { router.push(`/dashboard/${dashboard?.id}/edit`); }; - const navigateTo = (href: string) => { - router.push(href); - handleUserInfoClick(); - }; - return (
@@ -63,18 +52,7 @@ export default function Header({ component: Component }: HeaderProps) { <Component /> </div> )} - <div className={styles.userInfoContainer}> - <Button className={styles.userInfoButton} onClick={handleUserInfoClick}> - <UserInfo /> - </Button> - {isMenuVisible && ( - <div className={styles.myMenu}> - <div onClick={() => navigateTo('/mydashboard')}>내 대시보드</div> - <div onClick={() => navigateTo('/mypage')}>내 정보</div> - <div onClick={() => navigateTo('/')}>로그아웃</div> - </div> - )} - </div> + <UserSection /> </header> ); } diff --git a/src/components/header/UserSection.module.css b/src/components/header/UserSection.module.css new file mode 100644 index 0000000..b012e5b --- /dev/null +++ b/src/components/header/UserSection.module.css @@ -0,0 +1,30 @@ +.userInfoContainer { + position: relative; + display: flex; + align-items: center; +} + +.userInfoContainer::before { + content: ''; + border: 0.5px solid var(--gray-300); + height: 34px; +} + +.userInfoButton.userInfoButton { + margin-left: 16px; + display: flex; + align-items: center; + background-color: transparent; +} + +.myMenu { + position: absolute; + top: 75px; + right: 0; +} + +@media screen and (min-width: 768px) { + .myMenu { + top: 60px; + } +} diff --git a/src/components/header/UserSection.tsx b/src/components/header/UserSection.tsx new file mode 100644 index 0000000..61d163b --- /dev/null +++ b/src/components/header/UserSection.tsx @@ -0,0 +1,36 @@ +import UserInfo from './UserInfo'; +import Button from '../Button'; +import type { Menu } from '@/types/menu'; +import { useMenu } from '@/hooks/useMenu'; +import { useRouter } from 'next/navigation'; +import MenuDropdown from '../MenuDropdown'; +import styles from './UserSection.module.css'; + +export default function UserSection() { + const router = useRouter(); + const { isMenuVisible, toggleMenu, closeMenu } = useMenu(); + + const navigateTo = (href: string) => { + router.push(href); + closeMenu(); + }; + + const myInfoMenus: Menu[] = [ + { name: '내 대시보드', handleOnClick: () => navigateTo('/mydashboard') }, + { name: '내 정보', handleOnClick: () => navigateTo('/mypage') }, + { name: '로그아웃', handleOnClick: () => navigateTo('/') }, + ]; + + return ( + <div className={styles.userInfoContainer}> + <Button className={styles.userInfoButton} onClick={toggleMenu}> + <UserInfo /> + </Button> + {isMenuVisible && ( + <div className={styles.myMenu}> + <MenuDropdown menus={myInfoMenus} /> + </div> + )} + </div> + ); +} diff --git a/src/components/svg/Dot.tsx b/src/components/svg/Dot.tsx new file mode 100644 index 0000000..c9275aa --- /dev/null +++ b/src/components/svg/Dot.tsx @@ -0,0 +1,17 @@ +export default function Dot({ + width = '1em', + height = '1em', + fill = '#5534da', +}) { + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + width={width} + height={height} + viewBox="0 0 9 9" + fill="none" + > + <circle cx="4" cy="4.06836" r="4" fill={fill} /> + </svg> + ); +} diff --git a/src/components/svg/More.tsx b/src/components/svg/More.tsx new file mode 100644 index 0000000..3e7b202 --- /dev/null +++ b/src/components/svg/More.tsx @@ -0,0 +1,20 @@ +export default function More({ + width = '28', + height = '28', + fill = '#000000', +}) { + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + width={width} + height={height} + viewBox="0 0 28 28" + fill="none" + > + <path + d="M14 22.4809C13.5187 22.4809 13.1068 22.3096 12.7641 21.9669C12.4214 21.6242 12.25 21.2122 12.25 20.731C12.25 20.2497 12.4214 19.8378 12.7641 19.4951C13.1068 19.1524 13.5187 18.981 14 18.981C14.4812 18.981 14.8932 19.1524 15.2359 19.4951C15.5786 19.8378 15.7499 20.2497 15.7499 20.731C15.7499 21.2122 15.5786 21.6242 15.2359 21.9669C14.8932 22.3096 14.4812 22.4809 14 22.4809ZM14 15.7502C13.5187 15.7502 13.1068 15.5789 12.7641 15.2361C12.4214 14.8934 12.25 14.4815 12.25 14.0002C12.25 13.519 12.4214 13.107 12.7641 12.7643C13.1068 12.4216 13.5187 12.2503 14 12.2503C14.4812 12.2503 14.8932 12.4216 15.2359 12.7643C15.5786 13.107 15.7499 13.519 15.7499 14.0002C15.7499 14.4815 15.5786 14.8934 15.2359 15.2361C14.8932 15.5789 14.4812 15.7502 14 15.7502ZM14 9.01944C13.5187 9.01944 13.1068 8.84809 12.7641 8.50538C12.4214 8.16269 12.25 7.75072 12.25 7.26947C12.25 6.78824 12.4214 6.37627 12.7641 6.03357C13.1068 5.69088 13.5187 5.51953 14 5.51953C14.4812 5.51953 14.8932 5.69088 15.2359 6.03357C15.5786 6.37627 15.7499 6.78824 15.7499 7.26947C15.7499 7.75072 15.5786 8.16269 15.2359 8.50538C14.8932 8.84809 14.4812 9.01944 14 9.01944Z" + fill={fill} + /> + </svg> + ); +} diff --git a/src/components/svg/Pipe.tsx b/src/components/svg/Pipe.tsx new file mode 100644 index 0000000..7616977 --- /dev/null +++ b/src/components/svg/Pipe.tsx @@ -0,0 +1,13 @@ +export default function Pipe({ width = '2', height = '20', fill = '#D9D9D9' }) { + return ( + <svg + xmlns="http://www.w3.org/2000/svg" + width={width} + height={height} + viewBox="0 0 2 20" + fill="none" + > + <path d="M1 0V20" stroke={fill} /> + </svg> + ); +} diff --git a/src/hooks/useMenu.ts b/src/hooks/useMenu.ts new file mode 100644 index 0000000..3b22b19 --- /dev/null +++ b/src/hooks/useMenu.ts @@ -0,0 +1,15 @@ +import { useState } from 'react'; + +export function useMenu() { + const [isMenuVisible, setIsMenuVisible] = useState(false); + + const toggleMenu = () => { + setIsMenuVisible((prev) => !prev); + }; + + const closeMenu = () => { + setIsMenuVisible(false); + }; + + return { isMenuVisible, toggleMenu, closeMenu }; +} diff --git a/src/lib/cardService.ts b/src/lib/cardService.ts new file mode 100644 index 0000000..c20832d --- /dev/null +++ b/src/lib/cardService.ts @@ -0,0 +1,9 @@ +import axiosInstance from '@/lib/axiosInstance'; + +export const deleteCard = async (cardId: number) => { + try { + await axiosInstance.delete(`/cards/${cardId}`); + } catch (error) { + throw error; + } +}; diff --git a/src/types/dashboardView.ts b/src/types/dashboardView.ts index 616d62d..c4121d3 100644 --- a/src/types/dashboardView.ts +++ b/src/types/dashboardView.ts @@ -1,7 +1,7 @@ import { DebouncedFunc } from 'lodash'; export interface CardAssignee { - profileImageUrl: string; + profileImageUrl: string | null; nickname: string; id: number; } diff --git a/src/types/menu.ts b/src/types/menu.ts new file mode 100644 index 0000000..4af6062 --- /dev/null +++ b/src/types/menu.ts @@ -0,0 +1,4 @@ +export interface Menu { + name: string; + handleOnClick: (event: React.MouseEvent) => void; +} diff --git a/src/utils/colorUtils.ts b/src/utils/colorUtils.ts new file mode 100644 index 0000000..86c2def --- /dev/null +++ b/src/utils/colorUtils.ts @@ -0,0 +1,29 @@ +export const getRandomColor = (name: string) => { + const hash = [...name].reduce( + (acc, char) => acc + char.charCodeAt(0) * 31, + 0 + ); + const getValue = (offset: number) => { + const baseValue = (((hash >> offset) % 0xff) % 76) + 180; + const maxValue = 225; + return Math.min(baseValue, maxValue); + }; + const red = getValue(0); + const green = getValue(8); + const blue = getValue(16); + + return `rgb(${red}, ${green}, ${blue})`; +}; + +export const getDarkerColor = (rgbStr: string, adjustValue = 100) => { + const [red, green, blue] = extractRGBNumbers(rgbStr).map((color) => + Math.max(color - adjustValue, 0) + ); + + return `rgb(${red}, ${green}, ${blue})`; +}; + +export const extractRGBNumbers = (rgbStr: string): number[] => { + const match = rgbStr.match(/\d+/g); + return match ? match.map(Number) : []; +};