diff --git a/.storybook/main.ts b/.storybook/main.ts index 61fd3aa2..ed01c96e 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -15,6 +15,11 @@ const config: StorybookConfig = { docs: { autodocs: 'tag', }, + refs: { + '@chakra-ui/react': { + disable: true, + }, + }, webpackFinal: async config => { const imageRule = config.module?.rules?.find(rule => { const test = (rule as { test: RegExp }).test; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 1ee64805..8542c386 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -4,6 +4,7 @@ import { initialize, mswLoader } from 'msw-storybook-addon'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { Preview } from '@storybook/react'; +import Layout from '../src/v1/layout/Layout'; import ToastProvider from '../src/v1/base/Toast/ToastProvider'; import '@/styles/global.css'; diff --git a/package.json b/package.json index afe1915e..7ad013b0 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,10 @@ }, "dependencies": { "@actions/core": "^1.10.1", + "@chakra-ui/icons": "^2.0.17", + "@chakra-ui/react": "^2.4.9", + "@emotion/react": "^11.10.5", + "@emotion/styled": "^11.10.5", "@headlessui/react": "^1.7.15", "@tanstack/react-query": "^4.24.4", "@tanstack/react-query-devtools": "^4.24.12", @@ -26,12 +30,14 @@ "@types/react-dom": "18.0.10", "axios": "^1.3.4", "colorthief": "^2.4.0", + "framer-motion": "^9.0.2", "next": "13.4.7", "react": "18.2.0", "react-dom": "18.2.0", "react-error-boundary": "^3.1.4", "react-hook-form": "^7.43.2", - "react-intersection-observer": "^9.4.3" + "react-intersection-observer": "^9.4.3", + "recoil": "^0.7.7" }, "devDependencies": { "@babel/core": "^7.22.8", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 384b699f..ce432f46 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -49,6 +49,7 @@ export const metadata: Metadata = { const RootLayout = ({ children }: { children: React.ReactNode }) => { return ( + {/* @todo Chakra 제거시 app-layout 프로퍼티 제거. */} diff --git a/src/components/ChakraThemeProvider.tsx b/src/components/ChakraThemeProvider.tsx new file mode 100644 index 00000000..f6455931 --- /dev/null +++ b/src/components/ChakraThemeProvider.tsx @@ -0,0 +1,13 @@ +import theme from '@/styles/theme'; +import { ChakraProvider } from '@chakra-ui/react'; +import { NextPage } from 'next/types'; + +interface PropTypes { + children: React.ReactNode; +} + +const ChakraThemeProvider: NextPage = ({ children }) => { + return {children}; +}; + +export default ChakraThemeProvider; diff --git a/src/components/ContextProvider.tsx b/src/components/ContextProvider.tsx index 2527082b..749d52f1 100644 --- a/src/components/ContextProvider.tsx +++ b/src/components/ContextProvider.tsx @@ -1,8 +1,10 @@ 'use client'; import { ReactNode } from 'react'; +import { RecoilRoot } from 'recoil'; import PWAServiceWorkerProvider from '@/components/PWAServiceWorkerProvider'; +import ChakraThemeProvider from '@/components/ChakraThemeProvider'; import ReactQueryProvider from '@/components/ReactQueryProvider'; import ToastProvider from '@/v1/base/Toast/ToastProvider'; @@ -10,9 +12,13 @@ import ToastProvider from '@/v1/base/Toast/ToastProvider'; const ContextProvider = ({ children }: { children: ReactNode }) => { return ( - - {children} - + + + + {children} + + + ); }; diff --git a/src/styles/global.css b/src/styles/global.css index 6b1607b4..9e3cd4b6 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -21,14 +21,6 @@ html { @apply w-full bg-background font-[LineSeedKR] text-[62.5%] text-black-700; -webkit-tap-highlight-color: #ffffff50; - - /** from charka */ - -webkit-text-size-adjust: 100%; - font-family: system-ui, sans-serif; - -webkit-font-smoothing: antialiased; - text-rendering: optimizeLegibility; - -moz-osx-font-smoothing: grayscale; - touch-action: manipulation; } body { @@ -36,9 +28,9 @@ } .app-layout { + /* TODO: Chakra UI 걷어내면 제거 */ max-width: 43rem; margin: 0 auto; - @apply bg-white; } .sticky { diff --git a/src/styles/theme.tsx b/src/styles/theme.tsx new file mode 100644 index 00000000..1dd5222d --- /dev/null +++ b/src/styles/theme.tsx @@ -0,0 +1,115 @@ +import { + ChakraStyledOptions, + extendTheme, + ThemeOverride, +} from '@chakra-ui/react'; + +const fontSizes = { + xs: '1.2rem', + sm: '1.4rem', + md: '1.6rem', + lg: '1.8rem', + xl: '2rem', + '2xl': '2.2rem', +} as const; + +const buttonSizes = { + md: { + padding: '1.3rem 1rem', + height: '3.5rem', + }, + lg: { + padding: '2.5rem 1.8rem', + height: '4.5rem', + }, +} as const; + +const colors = { + main: '#F6AD55', // Main Theme + red: { + 800: '#F56565', // button (NoticeTheme) + 900: '#FF0000', // validation (NoticeTheme) + }, + yellow: { + 200: '#FFD4802E', + 900: '#FFA436', + }, + black: { + 400: '#C1C0C0', // subHeader (Slider) + 500: '#AFAFAF', // placeHolder (BookSearch) + 600: '#ACACAC', // placeHolder (MeetingEdit) + 700: '#727272', // subHeader (MeetingDetail) + 800: '#3D3D3D', // meetingPeriod (MeetingDetail) + 900: '#000000', // black + }, + white: { + 400: '#CFCFCF', // placeHolder (MyPage) + 500: '#D9D9D9', // addBook (MeetingCreate) + 600: '#E3E3E3', // bookBorder (Bookaive) + 700: '#E2E8F0', // inputBorder (Common) + 800: '#FAFAFA', // backGround + 900: '#FFFFFF', // white + }, + kakao: { + brown: '#191600', + yellow: '#fee102', + }, +} as const; + +interface SchemeTypings { + component: 'button'; + colorScheme: 'orange' | 'kakao' | 'orange-fill' | 'grey' | 'grey-fill'; + cssProps: ChakraStyledOptions; +} + +const scheme: Record< + SchemeTypings['component'], + Record> +> = { + button: { + orange: { + color: colors.main, + border: `${colors.main} 0.1rem solid`, + }, + 'orange-fill': { + color: colors.white[900], + backgroundColor: colors.main, + _hover: { + opacity: 0.8, + }, + }, + grey: { + color: colors.black[900], + border: `${colors.white[400]} 0.1rem solid`, + backgroundColor: colors.white[900], + _hover: { + color: colors.black['800'], + backgroundColor: colors.white[400], + }, + }, + 'grey-fill': { + color: colors.black[600], + backgroundColor: colors.white[400], + }, + kakao: { + color: colors.kakao.brown, + backgroundColor: colors.kakao.yellow, + }, + }, +}; + +const shadows = { + default: '0px 0px 7px -5px #000000', // BoxShadow (MeetingList Box) +}; + +const theme: ThemeOverride = extendTheme({ + fontSizes, + buttonSizes, + colors, + scheme, + shadows, +}); + +export default theme; + +export type ChakraTheme = typeof theme; diff --git a/src/ui/BookDetail/BookInfo.tsx b/src/ui/BookDetail/BookInfo.tsx new file mode 100644 index 00000000..0bf9530c --- /dev/null +++ b/src/ui/BookDetail/BookInfo.tsx @@ -0,0 +1,148 @@ +import { + Avatar, + AvatarGroup, + Box, + Flex, + Text, + useDisclosure, + useTheme, + VStack, +} from '@chakra-ui/react'; +import Image from 'next/image'; + +import IconButton from '@/ui/common/IconButton'; + +import type { APIBookDetail, APIBookmarkedUserList } from '@/types/book'; +import Link from 'next/link'; + +import { checkAuthentication } from '@/utils/helpers'; +import { useState } from 'react'; +import LoginBottomSheet from '../LoginBottomSheet'; + +type Props = Pick< + APIBookDetail, + 'title' | 'author' | 'contents' | 'imageUrl' | 'url' +> & + Omit & { + onBookmarkClick: (isBookMarked: boolean) => void; + }; + +const BookInfo = ({ + title, + author, + contents, + imageUrl, + url: contentsUrl, + onBookmarkClick, + ...bookmarkInfo +}: Props) => { + const isAuthenticated = checkAuthentication(); + const theme = useTheme(); + const [bookmark, setBookmark] = useState(bookmarkInfo.isInMyBookshelf); + + const { + isOpen: isLoginBottomSheetOpen, + onOpen: onLoginBottomSheetOpen, + onClose: onLoginBottomSheetsClose, + } = useDisclosure(); + + const handleBookmarkClick = () => { + if (!isAuthenticated) { + onLoginBottomSheetOpen(); + return; + } + + setBookmark(prev => { + const next = !prev; + onBookmarkClick(next); + return next; + }); + }; + + return ( + <> + + + book + + + + + {title} + + {author} + + + + {contents} ...  + {contentsUrl && ( + + 더보기 + + )} + + + {!isAuthenticated && ( + + )} + + + + {bookmarkInfo.users.map(({ userId, profileImage }) => ( + + ))} + + + {getUserInfoText(bookmarkInfo.totalCount, bookmarkInfo.users.length)} + + + + ); +}; + +const getUserInfoText = (totalCount: number, avatarCount: number) => { + const otherCount = totalCount - avatarCount; + + if (otherCount === 0 && totalCount === 0) { + return '아직 이 책을 책장에 꽂은 사람이 없어요.'; + } else if (otherCount === 0) { + return '님이 이 책을 책장에 꽂았어요.'; + } + + return `외 ${otherCount}명이 이 책을 책장에 꽂았어요.`; +}; + +export default BookInfo; diff --git a/src/ui/BookDetail/CommentDrawer.tsx b/src/ui/BookDetail/CommentDrawer.tsx new file mode 100644 index 00000000..2ef5612a --- /dev/null +++ b/src/ui/BookDetail/CommentDrawer.tsx @@ -0,0 +1,77 @@ +import { RefObject } from 'react'; +import { Drawer, DrawerContent, Flex, Text, Textarea } from '@chakra-ui/react'; + +import IconButton from '@/ui/common/IconButton'; + +interface Props { + title?: string; + placeholder?: string; + defaultComment?: string; + isOpen: boolean; + onClose: () => void; + onComplete: () => void; + textareaRef?: RefObject; +} + +const CommentDrawer = ({ + title, + placeholder, + defaultComment, + isOpen, + onClose, + onComplete, + textareaRef, +}: Props) => { + // TODO : ref로 textarea 포커싱 + return ( + + {isOpen && ( + + + + + {title} + + + 완료 + + +