From 73e142a0fda00437074b42f1f108c6e3327c0e30 Mon Sep 17 00:00:00 2001 From: Jonghyeon Ko Date: Mon, 18 Mar 2024 05:13:42 +0900 Subject: [PATCH] =?UTF-8?q?feat(people):=20=EC=B9=B4=EB=93=9C=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94=20/=20=EA=B9=83=ED=97=88=EB=B8=8C=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84,=20=EC=98=AC=EA=B0=80=EB=8B=88?= =?UTF-8?q?=EC=A0=9C=EC=9D=B4=EC=85=98=20url=20=EC=B6=94=EA=B0=80=20/=20?= =?UTF-8?q?=ED=8F=B0=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/people/package.json | 3 +- apps/people/src/app/components/Card.tsx | 481 ++++++++++++++++++++++++ apps/people/src/app/layout.tsx | 9 +- apps/people/src/app/page.tsx | 408 +------------------- apps/people/src/mock/cardDatas.ts | 58 +++ pnpm-lock.yaml | 17 + 6 files changed, 574 insertions(+), 402 deletions(-) create mode 100644 apps/people/src/app/components/Card.tsx create mode 100644 apps/people/src/mock/cardDatas.ts diff --git a/apps/people/package.json b/apps/people/package.json index d1314a3..4298a60 100644 --- a/apps/people/package.json +++ b/apps/people/package.json @@ -19,7 +19,8 @@ "framer-motion": "^11.0.14", "next": "^14.1.3", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "usehooks-ts": "^3.0.1" }, "devDependencies": { "@hamsurang/eslint-config": "workspace:*", diff --git a/apps/people/src/app/components/Card.tsx b/apps/people/src/app/components/Card.tsx new file mode 100644 index 0000000..240699f --- /dev/null +++ b/apps/people/src/app/components/Card.tsx @@ -0,0 +1,481 @@ +'use client' + +import { + FloatingArrow, + arrow, + autoUpdate, + flip, + offset, + shift, + useFloating, + useInteractions, + useRole, +} from '@floating-ui/react' +import { Box, Flex, Stack } from '@jsxcss/emotion' +import { SuspenseImage } from '@suspensive/react-image' +import { + type MotionValue, + motion, + useMotionValue, + useTransform, +} from 'framer-motion' +import { + type ReactElement, + cloneElement, + createContext, + useEffect, + useRef, + useState, +} from 'react' + +const hologramVerticals = [1, 2, 3, 4, 12, 3, 1, 2, 12, 3] as const + +const IsOpenContext = createContext(true) + +export type CardData = { + name: string + imageSrc: string + description: string + githubUsername: string +} + +export const Card = ({ + cardData, + onTap, + orderFromCenter, +}: { + cardData: CardData + selected: boolean + onTap: () => void + orderFromCenter: number +}) => { + const [isOpen, setIsOpen] = useState(true) + const [isHoverCard, setIsHoverCard] = useState(false) + + useEffect(() => { + const timeout = setTimeout( + () => setIsOpen(false), + 1000 + orderFromCenter * 30 + ) + return () => clearTimeout(timeout) + }, [orderFromCenter]) + + const mouseXFromCardCenter = useMotionValue(0) + const mouseYFromCardCenter = useMotionValue(0) + const rotateX = useTransform(mouseXFromCardCenter, [-100, 100], [15, -15]) + const rotateY = useTransform(mouseYFromCardCenter, [-100, 100], [-15, 15]) + + const cardSize = { width: 5 * 40, height: 7 * 40 } + const shineSize = 400 + + return ( + + setIsHoverCard(true)} + onHoverEnd={() => setIsHoverCard(false)} + onTap={onTap} + position="relative" + as={motion.div} + backgroundColor="#4A9BBE" + borderRadius={10} + overflow="hidden" + border="8px solid #debb2f" + boxSizing="border-box" + initial={{ + boxShadow: '0 0 30px 10px #00000030', + marginRight: -60, + rotateZ: 0, + y: 0, + scale: 0.6, + }} + animate={{ + boxShadow: isOpen + ? '0 0 40px 30px #ffcc002f' + : '0 0 30px 10px #00000030', + marginRight: isOpen ? 0 : -60, + scale: isOpen ? 1.4 : 1, + rotateZ: isOpen ? orderFromCenter * 6 : 0, + y: isOpen ? Math.abs(orderFromCenter) * 20 + -100 : 0, + }} + whileHover={{ + rotateZ: orderFromCenter * 8, + y: -80, + zIndex: 90, + boxShadow: '0 0 40px 30px #ffcc002f', + scale: 2.8, + }} + style={{ + ...cardSize, + rotateX, + rotateY, + }} + onMouseMove={(e) => { + const rect = e.currentTarget.getBoundingClientRect() + const centerX = rect.left + (rect.right - rect.left) / 2 + const centerY = rect.top + (rect.bottom - rect.top) / 2 + mouseXFromCardCenter.set(-(centerX - e.pageX)) + mouseYFromCardCenter.set(-(centerY - e.pageY)) + }} + onMouseLeave={() => { + mouseXFromCardCenter.set(0) + mouseYFromCardCenter.set(0) + }} + padding="8px 8px" + > + + + + + 함수랑 + + + {cardData.name}몬 + + + + + + + Lv + + + 60 + + + + + + + + + + + + + + GitHub 프로필({cardData.githubUsername})로 연결됩니다. + + + + + + Climber + 1기 + + + {cardData.description} + + + + + + +
weakness
+
resistance
+
+ +
weakness
+
+
+ + + + Basic + + Basic Level + + + Looked up one of the more obscure Latin words, consectetur, from + a Lorem Ipsum passage, and going through + + +
+
+ + + +
+
+ ) +} + +const HologramVertical = ({ + cardSize, + mouseXFromCardCenter, +}: { + cardSize: { width: number; height: number } + mouseXFromCardCenter: MotionValue +}) => { + const glare1X = useTransform( + mouseXFromCardCenter, + [-100, 100], + [0 - cardSize.width / 2, cardSize.width / 2] + ) + const glare2X = useTransform( + mouseXFromCardCenter, + [-100, 100], + [cardSize.width / 2, 0 - cardSize.width / 2] + ) + + return ( + <> + + {hologramVerticals.map((width, index) => ( + + ))} + + + {hologramVerticals.map((width, index) => ( + + ))} + + + ) +} + +const Shine = ({ + mouseXFromCardCenter, + mouseYFromCardCenter, + size, + enabled, +}: { + mouseXFromCardCenter: MotionValue + mouseYFromCardCenter: MotionValue + size: number + enabled: boolean +}) => { + return ( + + + + + + + + ) +} + +export const Tooltip = ({ children }: { children: ReactElement }) => { + const tooltip = useDisclosure({ + initialState: true, + }) + const tooltipArrowRef = useRef(null) + const tooltipFloating = useFloating({ + open: tooltip.isOpen, + onOpenChange: (open) => (open ? tooltip.open() : tooltip.close()), + middleware: [ + offset(10), + flip(), + shift(), + arrow({ element: tooltipArrowRef }), + ], + whileElementsMounted: autoUpdate, + }) + const tooltipInteractions = useInteractions([ + useRole(tooltipFloating.context, { role: 'tooltip' }), + ]) + return ( + <> + {cloneElement( + children, + tooltipInteractions.getReferenceProps({ + ref: tooltipFloating.refs.setReference, + }) + )} +
+ hihihi + +
+ + ) +} + +export const useDisclosure = (options: { + initialState?: boolean + onOpen?: () => void + onClose?: () => void +}) => { + const { initialState = false, onOpen, onClose } = options + + const [isOpen, setIsOpen] = useState(initialState) + + const open = () => { + setIsOpen(true) + + return onOpen?.() + } + + const close = () => { + setIsOpen(false) + + return onClose?.() + } + + const toggle = () => (isOpen ? close() : open()) + + return { isOpen, open, close, toggle } +} diff --git a/apps/people/src/app/layout.tsx b/apps/people/src/app/layout.tsx index 0abaf3c..8529138 100644 --- a/apps/people/src/app/layout.tsx +++ b/apps/people/src/app/layout.tsx @@ -1,6 +1,13 @@ import type { Metadata } from 'next' +import { Inter } from 'next/font/google' import Providers from './providers' +// If loading a variable font, you don't need to specify the font weight +const inter = Inter({ + subsets: ['latin'], + display: 'swap', +}) + export const metadata: Metadata = { title: 'hamsurang - people', } @@ -11,7 +18,7 @@ export default function RootLayout({ children: React.ReactNode }) { return ( - + {children} diff --git a/apps/people/src/app/page.tsx b/apps/people/src/app/page.tsx index e4dc06d..032bb6d 100644 --- a/apps/people/src/app/page.tsx +++ b/apps/people/src/app/page.tsx @@ -1,17 +1,18 @@ 'use client' -import { useFloating, useHover, useInteractions } from '@floating-ui/react' import { Box, Flex, Stack } from '@jsxcss/emotion' import { Suspense } from '@suspensive/react' import { SuspenseImage } from '@suspensive/react-image' -import { Reorder, motion, useMotionValue, useTransform } from 'framer-motion' -import { useEffect, useState } from 'react' +import { Reorder, motion } from 'framer-motion' +import { useState } from 'react' +import { Card, type CardData } from './components/Card' +import { initialCards } from '~/mock/cardDatas' export default function Home() { const [cards, setCards] = useState(initialCards) - const [selectedCardName, setSelectedCardName] = useState( - null - ) + const [selectedCardName, setSelectedCardName] = useState< + CardData['name'] | null + >(null) return ( ( setSelectedCardName(card.name)} orderFromCenter={cardsIndex - cardsArray.length / 2} @@ -61,396 +62,3 @@ export default function Home() { ) } - -const Card = ({ - imageSrc, - name, - onTap, - orderFromCenter, - description, -}: Card & { - selected: boolean - onTap: () => void - orderFromCenter: number -}) => { - const [isOpen, setIsOpen] = useState(true) - const { refs, context } = useFloating({ - open: isOpen, - onOpenChange: setIsOpen, - }) - const { getReferenceProps, getFloatingProps } = useInteractions([ - useHover(context), - ]) - - useEffect(() => { - const timeout = setTimeout(() => setIsOpen(false), 1000) - return () => clearTimeout(timeout) - }, []) - - const x = useMotionValue(0) - const y = useMotionValue(0) - const rotateX = useTransform(x, [-100, 100], [15, -15]) - const rotateY = useTransform(y, [-100, 100], [-15, 15]) - - const cardSize = { width: 200, height: 280 } - const shineSize = 400 - const shineX = useTransform( - x, - [-100, 100], - [0 - shineSize / 2, 200 - shineSize / 2] - ) - const shineY = useTransform( - y, - [-100, 100], - [0 - shineSize / 2, 280 - shineSize / 2] - ) - const glare1X = useTransform( - x, - [-100, 100], - [0 - cardSize.width / 2, cardSize.width / 2] - ) - const glare2X = useTransform( - x, - [-100, 100], - [cardSize.width / 2, 0 - cardSize.width / 2] - ) - - return ( - { - const rect = e.currentTarget.getBoundingClientRect() - const centerX = rect.left + (rect.right - rect.left) / 2 - const centerY = rect.top + (rect.bottom - rect.top) / 2 - x.set(-(centerX - e.pageX)) - y.set(-(centerY - e.pageY)) - }} - onTap={onTap} - cursor="pointer" - initial={{ rotateZ: 0, y: 0 }} - animate={ - isOpen - ? { rotateZ: orderFromCenter * 8, y: orderFromCenter * 20 } - : { rotateZ: 0, y: 0 } - } - whileHover={{ - y: -80, - scale: 2, - zIndex: 9, - }} - > - { - x.set(0) - y.set(0) - }} - padding="8px 8px" - > - - - - - Hamsurang - - - {name} - - - - - - - Lv - - - 60 - - - - - - - - - - - - - - It is a long established fact that a reader will be distracted - - - - - - Climber - 1기 - - - {description} - - - -
weakness
-
resistance
-
- -
weakness
-
-
- - - - Basic - - Basic Level - - - Looked up one of the more obscure Latin words, consectetur, from - a Lorem Ipsum passage, and going through - - -
-
- - - - - - - - - - {hologramVerticals.map((width, index) => ( - - ))} - - - {hologramVerticals.map((width, index) => ( - - ))} - -
-
- ) -} - -type Card = { - name: string - imageSrc: string - description: string -} - -const hologramVerticals = [1, 2, 3, 4, 12, 3, 1, 2, 12, 3] as const - -const initialCards = [ - { - name: '웨일', - imageSrc: 'https://avatars.githubusercontent.com/u/71202076?v=4', - description: - 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem', - }, - { - name: '쏘니', - imageSrc: 'https://avatars.githubusercontent.com/u/47546413?v=4', - description: - 'Making it over 2000 years old. Richard McClintock, consectetur, from a Lorem Ipsum passage, and going through', - }, - { - name: '민수르', - imageSrc: 'https://avatars.githubusercontent.com/u/40910757?v=4', - description: - 'All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet.', - }, - { - name: '민초당', - imageSrc: 'https://avatars.githubusercontent.com/u/90169703?v=4', - description: - 'Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which', - }, - { - name: '마누', - imageSrc: 'https://avatars.githubusercontent.com/u/61593290?v=4', - description: - 'The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from', - }, - { - name: '쿼카', - imageSrc: 'https://avatars.githubusercontent.com/u/57122180?v=4', - description: - 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem', - }, - { - name: '퉁이리', - imageSrc: 'https://avatars.githubusercontent.com/u/77133565?v=4', - description: - 'Looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through', - }, - { - name: '모리', - imageSrc: 'https://avatars.githubusercontent.com/u/89721027?v=4', - description: - 'It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which', - }, - { - name: '쉽', - imageSrc: 'https://avatars.githubusercontent.com/u/43772082?v=4', - description: - 'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem', - }, -] satisfies Card[] diff --git a/apps/people/src/mock/cardDatas.ts b/apps/people/src/mock/cardDatas.ts new file mode 100644 index 0000000..e9f6311 --- /dev/null +++ b/apps/people/src/mock/cardDatas.ts @@ -0,0 +1,58 @@ +import type { CardData } from '~/app/components/Card' + +export const initialCards = [ + { + name: '웨일', + imageSrc: 'https://avatars.githubusercontent.com/u/71202076?v=4', + description: '오늘 칼국수를 먹었습니다.', + githubUsername: '2-NOW', + }, + { + name: '쏘니', + imageSrc: 'https://avatars.githubusercontent.com/u/47546413?v=4', + description: '후후.. 헬린이여서 3대 30입니다.', + githubUsername: 'sonsurim', + }, + { + name: '민수르', + imageSrc: 'https://avatars.githubusercontent.com/u/40910757?v=4', + description: '꾸준히 하는거 잘합니다.', + githubUsername: 'minsour', + }, + { + name: '민초당', + imageSrc: 'https://avatars.githubusercontent.com/u/90169703?v=4', + description: '세상사에 다양하게 관심이 많습니다.', + githubUsername: 'minchodang', + }, + { + name: '마누', + imageSrc: 'https://avatars.githubusercontent.com/u/61593290?v=4', + description: '지속적인 플러팅 ENTP입니다.', + githubUsername: 'manudeli', + }, + { + name: '쿼카', + imageSrc: 'https://avatars.githubusercontent.com/u/57122180?v=4', + description: '단축키 진짜 많이 압니다.', + githubUsername: 'minsoo-web', + }, + { + name: '퉁이리', + imageSrc: 'https://avatars.githubusercontent.com/u/77133565?v=4', + description: '토마토 스파게티 좋아합니다.', + githubUsername: 'tooooo1', + }, + { + name: '모리', + imageSrc: 'https://avatars.githubusercontent.com/u/89721027?v=4', + description: '좋아하는 음식은 대부분 다 좋아합니다.', + githubUsername: 'chaaerim', + }, + { + name: '쉽', + imageSrc: 'https://avatars.githubusercontent.com/u/43772082?v=4', + description: '클라이밍 1년째 하지만, 여전히 초보에서 벗어나지 못합니다.', + githubUsername: 'yejineee', + }, +] satisfies CardData[] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73547f5..270ffdf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + usehooks-ts: + specifier: ^3.0.1 + version: 3.0.1(react@18.2.0) devDependencies: '@hamsurang/eslint-config': specifier: workspace:* @@ -2929,6 +2932,10 @@ packages: dependencies: p-locate: 5.0.0 + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: false + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -4047,6 +4054,16 @@ packages: react: 18.2.0 dev: false + /usehooks-ts@3.0.1(react@18.2.0): + resolution: {integrity: sha512-bgJ8S9w/SnQyACd3RvWp3CGncROxEENGqQLCsdaoyTb0zTENIna7MIV3OW6ywCfPaYYD2OPokw7oLPmSLLWP4w==} + engines: {node: '>=16.15.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + lodash.debounce: 4.0.8 + react: 18.2.0 + dev: false + /vscode-languageserver-textdocument@1.0.11: resolution: {integrity: sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==} dev: false