Skip to content

Commit

Permalink
Merge pull request #87 from DDD-Community/feature/76
Browse files Browse the repository at this point in the history
[FEATURE/76] 온보딩 튜토리얼
  • Loading branch information
hwanheejung authored Aug 15, 2024
2 parents e65bb87 + dbf1c40 commit 2abb3c1
Show file tree
Hide file tree
Showing 11 changed files with 466 additions and 35 deletions.
52 changes: 52 additions & 0 deletions public/icons/sticker_polaroid.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 13 additions & 1 deletion src/app/(board)/board/[boardId]/components/OpenModalBtn.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
'use client'

import Modal from '@/components/Modal'
import { useSession } from 'next-auth/react'
import AddPolaroid from 'public/icons/add_polaroid.svg'
import { ReactNode } from 'react'
import { useModal } from './CreatePolaroidModal/ModalContext'
import CannotUploadModal from './modals/CannotUploadModal'
import Tutorial from './Tutorial'
import { Step2Tooltip } from './Tutorial/Tooltips'

interface OpenModalBtnProps {
polaroidNum: number
children: ReactNode
}

const OpenModalBtn = ({ polaroidNum, children }: OpenModalBtnProps) => {
const { data: session } = useSession()
const { isOpen, openModal, closeModal } = useModal()

const renderModalContent = () => {
Expand All @@ -28,7 +32,15 @@ const OpenModalBtn = ({ polaroidNum, children }: OpenModalBtnProps) => {
return (
<div>
{isOpen && renderModalContent()}
<AddPolaroid onClick={openModal} className="absolute bottom-10 right-4" />
<div className="absolute bottom-10 right-4">
<Tutorial
step={session ? 2 : 1}
tooltip={<Step2Tooltip />}
hasNext={false}
>
<AddPolaroid onClick={openModal} />
</Tutorial>
</div>
</div>
)
}
Expand Down
11 changes: 10 additions & 1 deletion src/app/(board)/board/[boardId]/components/ShareBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import CopyIcon from 'public/icons/copy.svg'
import Share from 'public/icons/ios_share.svg'
import TwoPolaroidsIcon from 'public/icons/twopolaroids.svg'
import { useEffect, useState } from 'react'
import { useTutorial } from './Tutorial/TutorialContext'

const ShareBtn = () => {
const [showShareModal, setShowShareModal] = useState<boolean>(false)
Expand All @@ -18,11 +19,19 @@ const ShareBtn = () => {
return navigator.clipboard.writeText(currentURL)
}

const { run, nextStep } = useTutorial()
const handleClose = () => {
setShowShareModal(false)
if (run) {
nextStep()
}
}

return (
<>
<Share onClick={() => setShowShareModal(true)} className="w-6" />

<Modal isOpen={showShareModal} onClose={() => setShowShareModal(false)}>
<Modal isOpen={showShareModal} onClose={handleClose}>
<Modal.CenterModal icon={<TwoPolaroidsIcon />}>
<Modal.Close />
<Modal.Title>보드를 친구에게 공유해보세요!</Modal.Title>
Expand Down
95 changes: 95 additions & 0 deletions src/app/(board)/board/[boardId]/components/Tutorial/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use client'

import { ReactNode } from 'react'
import { twMerge } from 'tailwind-merge'
import { useTutorial } from './TutorialContext'

const Tooltip = ({
children,
className = '',
}: {
children: ReactNode
className?: React.ComponentProps<'div'>['className']
}) => {
return <div className={twMerge(`relative`, className)}>{children}</div>
}

const Triangle = ({
className = '',
}: {
className?: React.ComponentProps<'div'>['className']
}) => (
<div className={twMerge('absolute right-3 -z-10', className)}>
<div className="h-8 w-8 rotate-[30deg] skew-y-[30deg] scale-x-[0.866] transform rounded-lg bg-gray-0" />
</div>
)

const Box = ({
children,
className = '',
trianglePos = '',
}: {
children: ReactNode
className?: React.ComponentProps<'div'>['className']
trianglePos?: React.ComponentProps<'div'>['className']
}) => {
return (
<div
className={twMerge(
'absolute right-0 top-0 z-20 flex flex-col items-end justify-end rounded-md bg-gray-0',
className,
)}
>
<Triangle className={trianglePos} />
{children}
</div>
)
}

const Content = ({ children }: { children: ReactNode }) => {
return (
<div className="mb-[3px] whitespace-pre-line text-center text-md font-semiBold">
{children}
</div>
)
}

const Icon = ({
icon,
sendToBack = false,
className = '',
}: {
icon: ReactNode
sendToBack?: boolean
className?: React.ComponentProps<'div'>['className']
}) => {
return (
<div
className={twMerge(
`absolute -top-[0%] left-0 -translate-x-1/2 -translate-y-1/2 ${sendToBack ? 'z-10' : 'z-40'}`,
className,
)}
>
{icon}
</div>
)
}

const NextBtn = ({ hasNext }: { hasNext: boolean }) => {
const { nextStep, endTutorial } = useTutorial()
return (
<div
onClick={hasNext ? nextStep : endTutorial}
className="cursor-pointer text-right text-sm font-semiBold text-negative"
>
{hasNext ? '다음' : '확인'}
</div>
)
}

Tooltip.Box = Box
Tooltip.Icon = Icon
Tooltip.Content = Content
Tooltip.NextBtn = NextBtn

export default Tooltip
44 changes: 44 additions & 0 deletions src/app/(board)/board/[boardId]/components/Tutorial/Tooltips.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client'

import Step1Icon from 'public/icons/linkShare.svg'
import Step2Icon from 'public/icons/sticker_polaroid.svg'
import Tooltip from './Tooltip'

export const Step1Tooltip = () => {
return (
<Tooltip className="absolute right-4 top-[115%]">
<Tooltip.Icon
icon={<Step1Icon className="scale-150" />}
sendToBack
className="-left-[270px]"
/>
<Tooltip.Box
className="w-[270px] px-[18px] py-[15px]"
trianglePos="-top-[0%] -translate-y-[20%]"
>
<Tooltip.Content>친구들에게 보드를 공유해보세요!</Tooltip.Content>
<Tooltip.NextBtn hasNext />
</Tooltip.Box>
</Tooltip>
)
}

export const Step2Tooltip = () => {
return (
<Tooltip className="absolute -top-[220%] right-4">
<Tooltip.Icon
icon={<Step2Icon />}
className="-left-[130px] -translate-y-3/4"
/>
<Tooltip.Box
className="w-[260px] px-[18px] py-5"
trianglePos="-bottom-[0%] translate-y-[20%]"
>
<Tooltip.Content>
{`보드 주제와 맞는\n 사진을 올려 보드를 꾸며주세요!`}
</Tooltip.Content>
<Tooltip.NextBtn hasNext={false} />
</Tooltip.Box>
</Tooltip>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use client'

import {
ReactNode,
createContext,
useContext,
useEffect,
useMemo,
useState,
} from 'react'

interface TutorialContextProps {
run: boolean
currentStep: number
nextStep: () => void
startTutorial: () => void
endTutorial: () => void
}

const TutorialContext = createContext<TutorialContextProps | undefined>(
undefined,
)

export const TutorialProvider = ({ children }: { children: ReactNode }) => {
const [run, setRun] = useState<boolean>(false)
const [currentStep, setCurrentStep] = useState<number>(1)

const nextStep = () => setCurrentStep((prev) => prev + 1)

const startTutorial = () => setRun(true)
const endTutorial = () => {
setRun(false)
localStorage.setItem('needTutorial', 'false')
}

useEffect(() => {
if (localStorage.getItem('needTutorial') === 'true') {
startTutorial()
}
}, [])

const value = useMemo(
() => ({
run,
currentStep,
nextStep,
startTutorial,
endTutorial,
}),
[currentStep, run],
)

return (
<TutorialContext.Provider value={value}>
{children}
</TutorialContext.Provider>
)
}

export const useTutorial = () => {
const context = useContext(TutorialContext)
if (context === undefined) {
throw new Error('Error at useTutorial')
}
return context
}
108 changes: 108 additions & 0 deletions src/app/(board)/board/[boardId]/components/Tutorial/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
'use client'

import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react'
import { useTutorial } from './TutorialContext'

interface TutorialProps {
children: ReactNode
step: number
tooltip: ReactNode
hasNext?: boolean
}

const Tutorial = ({
step,
tooltip,
hasNext = true,
children,
}: TutorialProps) => {
const targetRef = useRef<HTMLDivElement>(null)
const [isOpen, setIsOpen] = useState<boolean>(false)
const [overlayStyle, setOverlayStyle] = useState<CSSProperties>({})
const { run, currentStep, endTutorial } = useTutorial()

// overlay 클릭 방지
const [topBox, setTopBox] = useState<CSSProperties>({})
const [bottomBox, setBottomBox] = useState<CSSProperties>({})
const [leftBox, setLeftBox] = useState<CSSProperties>({})
const [rightBox, setRightBox] = useState<CSSProperties>({})

useEffect(() => {
if (run && step === currentStep) {
setIsOpen(true)
} else {
setIsOpen(false)
}
}, [run, step, currentStep])

useEffect(() => {
if (isOpen && targetRef.current) {
const targetRect = targetRef.current.getBoundingClientRect()

setOverlayStyle({
position: 'fixed',
top: targetRect.top - 2,
left: targetRect.left - 3,
width: targetRect.width + 6,
height: targetRect.height + 7,
borderRadius: '50%',
zIndex: 10,
boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.6)',
pointerEvents: 'none',
})

setTopBox({
bottom: `calc(100% - ${targetRect.top}px + 2px)`,
})
setBottomBox({
top: targetRect.bottom + 5,
})
setLeftBox({
right: `calc(100% - ${targetRect.left}px + 3px)`,
})
setRightBox({
left: targetRect.right + 3,
})
}
}, [isOpen])

const handleTargetClick = () => {
setIsOpen(false)
if (!hasNext) {
endTutorial()
}
}

return (
<>
<div ref={targetRef} onClick={handleTargetClick}>
{children}
</div>
{isOpen && (
<>
<div style={overlayStyle}>
<div
className="pointer-events-auto fixed left-0 h-dvh w-screen"
style={topBox}
/>
<div
className="pointer-events-auto fixed left-0 h-dvh w-screen"
style={bottomBox}
/>
<div
className="pointer-events-auto fixed top-0 h-dvh w-screen"
style={leftBox}
/>
<div
className="pointer-events-auto fixed top-0 h-dvh w-screen"
style={rightBox}
/>
</div>
{tooltip}
</>
)}
</>
)
}

export default Tutorial
Loading

0 comments on commit 2abb3c1

Please sign in to comment.