diff --git a/next.config.ts b/next.config.ts index e9ffa30..0ccb075 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,9 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; const nextConfig: NextConfig = { - /* config options here */ + images: { + domains: ['sprint-fe-project.s3.ap-northeast-2.amazonaws.com'], + }, }; export default nextConfig; diff --git a/src/app/(with-header-sidebar)/dashboard/[id]/page.tsx b/src/app/(with-header-sidebar)/dashboard/[id]/page.tsx new file mode 100644 index 0000000..9ea98f6 --- /dev/null +++ b/src/app/(with-header-sidebar)/dashboard/[id]/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return
dashboard page
; +} diff --git a/src/app/(with-header-sidebar)/mydashboard/hooks/useApi.ts b/src/app/(with-header-sidebar)/mydashboard/hooks/useApi.ts index 3af9bdc..45b8381 100644 --- a/src/app/(with-header-sidebar)/mydashboard/hooks/useApi.ts +++ b/src/app/(with-header-sidebar)/mydashboard/hooks/useApi.ts @@ -31,15 +31,18 @@ export default function useApi< const [error, setError] = useState(null); const fetchData = useCallback(async () => { + // TODO if (loading) return; add? setLoading(true); + setError(null); + + const config = { + method: options.method, + url, + params: options.params, + data: options.body, + }; try { - const config = { - method: options.method, - url, - params: options.params, - data: options.body, - }; const response: AxiosResponse = await axiosInstance.request(config); setData(response.data); } catch (err) { @@ -47,7 +50,7 @@ export default function useApi< } finally { setLoading(false); } - }, [url, options]); + }, [url, options.method, options.params, options.body]); useEffect(() => { fetchData(); diff --git a/src/app/(with-header-sidebar)/mydashboard/hooks/useWindowSize.ts b/src/app/(with-header-sidebar)/mydashboard/hooks/useWindowSize.ts index d7b6aaa..10a6e90 100644 --- a/src/app/(with-header-sidebar)/mydashboard/hooks/useWindowSize.ts +++ b/src/app/(with-header-sidebar)/mydashboard/hooks/useWindowSize.ts @@ -18,7 +18,7 @@ export default function useWindowSize(): WindowSize { setWindowSize({ width: window.innerWidth, height: window.innerHeight, - isMobile: window.innerWidth <= 768, + isMobile: window.innerWidth < 768, }); } diff --git a/src/app/favicon.ico b/src/app/favicon.ico index 718d6fe..ac28f4a 100644 Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 540ff9d..8535599 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -11,8 +11,8 @@ const pretendard = localFont({ }); export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', + title: 'Taskify', + description: 'Task management application', }; export default function RootLayout({ diff --git a/src/components/Button.module.css b/src/components/Button.module.css index 9d5de36..2c0dc85 100644 --- a/src/components/Button.module.css +++ b/src/components/Button.module.css @@ -5,6 +5,7 @@ font-size: 14px; font-weight: 600; color: var(--white); + white-space: nowrap; } .button:disabled { diff --git a/src/components/Header.module.css b/src/components/Header.module.css index c646ab7..ea977d4 100644 --- a/src/components/Header.module.css +++ b/src/components/Header.module.css @@ -1,3 +1,91 @@ .header { + padding: 16px; border-bottom: 1px solid var(--gray-300); + display: flex; + justify-content: space-around; + align-items: center; + gap: 16px; +} + +.title { + flex: 1; + color: var(--black-100); + font-size: 16px; + font-weight: 700; +} + +.buttonContainer { + display: flex; + justify-content: space-between; + align-items: center; + gap: 6px; +} + +.button { + padding: 6px 12px; + display: flex; + align-items: center; + gap: 8px; + background-color: transparent; + border-radius: 6px; + border: 1px solid var(--gray-300); + color: var(--gray-500); + font-size: 14px; + font-weight: 500; +} + +.icon { + display: none; +} + +.userInfoContainer { + display: flex; + align-items: center; +} + +.userInfoContainer::before { + content: ''; + border: 0.5px solid var(--gray-300); + height: 34px; +} + +.userInfoWrapper { + margin-left: 16px; + display: flex; + align-items: center; +} + +@media screen and (min-width: 768px) { + .header { + padding-left: 40px; + padding-right: 30px; + gap: 36px; + } + + .title { + font-size: 20px; + } + + .buttonContainer { + gap: 16px; + } + + .button { + padding: 10px 16px; + font-size: 16px; + } + + .userInfoWrapper { + margin-left: 36px; + } +} + +@media screen and (min-width: 1199px) { + .header { + padding-right: 80px; + } + + .icon { + display: block; + } } diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 182e1a4..ef44bfa 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,20 +1,52 @@ 'use client'; import { usePathname } from 'next/navigation'; +import Image from 'next/image'; +import Button from './Button'; +import UserInfo from './UserInfo'; import styles from './Header.module.css'; -import type { User } from '@/app/(with-header-sidebar)/mydashboard/types/user'; -const user: User = { - id: 1, - email: 'heejin@gmail.com', - nickname: 'heejin', - profileImageUrl: null, - createdAt: '2024-11-15T14:29:07.482Z', -}; +interface HeaderProps { + component: React.ComponentType; +} -export default function Header() { +export default function Header({ component: Component }: HeaderProps) { const pathname = usePathname(); - const { nickname, profileImageUrl } = user; - - return
내 대시보드
; + return ( +
+ 내 대시보드 +
+ + +
+ {Component && ( +
+ +
+ )} +
+
+ +
+
+
+ ); } diff --git a/src/components/SideBar.module.css b/src/components/SideBar.module.css index 9d6700a..5b07566 100644 --- a/src/components/SideBar.module.css +++ b/src/components/SideBar.module.css @@ -5,9 +5,15 @@ } .button { - background-color: transparent !important; + background-color: transparent; } .active { color: salmon; } + +@media screen and (min-width: 768px) { + .sideBar { + min-width: 155px; + } +} diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index 54fb51c..bb6e058 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -2,10 +2,10 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import styles from './SideBar.module.css'; import Image from 'next/image'; import Button from './Button'; import useWindowSize from '@/app/(with-header-sidebar)/mydashboard/hooks/useWindowSize'; +import styles from './SideBar.module.css'; export default function SideBar() { const pathname = usePathname(); diff --git a/src/components/UserInfo.module.css b/src/components/UserInfo.module.css new file mode 100644 index 0000000..19bd968 --- /dev/null +++ b/src/components/UserInfo.module.css @@ -0,0 +1,25 @@ +.image { + border-radius: 50%; + object-fit: cover; +} + +.userInfo { + display: flex; + align-items: center; + gap: 15px; +} + +.userIcon { + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; +} + +.nickname { + color: var(--black-100); + font-size: 16px; + font-weight: 500; +} diff --git a/src/components/UserInfo.tsx b/src/components/UserInfo.tsx new file mode 100644 index 0000000..f51e6ef --- /dev/null +++ b/src/components/UserInfo.tsx @@ -0,0 +1,76 @@ +'use client'; + +import type { User } from '@/app/(with-header-sidebar)/mydashboard/types/user'; +import styles from './UserInfo.module.css'; +import Image from 'next/image'; +import useWindowSize from '@/app/(with-header-sidebar)/mydashboard/hooks/useWindowSize'; +import { useState, useEffect } from 'react'; +import UserInfoSkeleton from './skeleton/UserInfoSkeleton'; + +const user: User = { + id: 1, + email: 'heejin@gmail.com', + nickname: 'heejin', + // profileImageUrl: + // 'https://sprint-fe-project.s3.ap-northeast-2.amazonaws.com/taskify/profile_image/10-1_4804_1731757528194.jpeg', + profileImageUrl: null, + createdAt: '2024-11-15T14:29:07.482Z', +}; + +export default function UserInfo() { + const { email, nickname, profileImageUrl } = user; + const { isMobile } = useWindowSize(); + + const [colors, setColors] = useState<{ + randomColor: string; + invertedColor: string; + } | null>(null); + + useEffect(() => { + const { randomColor, invertedColor } = getRandomColor(); + setColors({ randomColor, invertedColor }); + }, []); + + if (!colors) { + return ; + } + + return ( + <> + {profileImageUrl ? ( + 프로필 이미지 + ) : ( +
+
+ {email[0].toUpperCase()} +
+ {!isMobile && {nickname}} +
+ )} + + ); +} + +function getRandomColor() { + const randomColor = `#${Math.floor(Math.random() * 16777215).toString(16)}`; + + const r = parseInt(randomColor.slice(1, 3), 16); + const g = parseInt(randomColor.slice(3, 5), 16); + const b = parseInt(randomColor.slice(5, 7), 16); + + const invertedColor = `rgb(${255 - r}, ${255 - g}, ${255 - b})`; + + return { randomColor, invertedColor }; +} diff --git a/src/components/skeleton/UserInfoSkeleton.module.css b/src/components/skeleton/UserInfoSkeleton.module.css new file mode 100644 index 0000000..4afea0f --- /dev/null +++ b/src/components/skeleton/UserInfoSkeleton.module.css @@ -0,0 +1,23 @@ +.userIcon { + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--gray-300); +} + +@media screen and (min-width: 768px) { + .userInfo { + display: flex; + align-items: center; + gap: 15px; + } + + .nickname { + background-color: var(--gray-300); + width: 45px; + height: 20px; + } +} diff --git a/src/components/skeleton/UserInfoSkeleton.tsx b/src/components/skeleton/UserInfoSkeleton.tsx new file mode 100644 index 0000000..c12ab80 --- /dev/null +++ b/src/components/skeleton/UserInfoSkeleton.tsx @@ -0,0 +1,10 @@ +import styles from './UserInfoSkeleton.module.css'; + +export default function UserInfoSkeleton() { + return ( +
+
+ +
+ ); +}