Skip to content

Commit

Permalink
Merge pull request #140 from DDD-Community/feature/screenshot
Browse files Browse the repository at this point in the history
[FEATURE] 보드 공유를 위한 폴라로이드 선택, 스크린샷 기능 추가
  • Loading branch information
junseublim authored Dec 18, 2024
2 parents 059ef7e + e536848 commit d39260a
Show file tree
Hide file tree
Showing 32 changed files with 1,379 additions and 135 deletions.
690 changes: 669 additions & 21 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"next": "14.2.4",
"next-auth": "^5.0.0-beta.20",
"prettier-plugin-tailwindcss": "^0.6.5",
"puppeteer": "^23.10.4",
"react": "^18",
"react-device-detect": "^2.2.3",
"react-dom": "^18",
Expand Down
5 changes: 5 additions & 0 deletions public/icons/PinIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@ import { getPolaroidNickname } from '@/lib/utils/polaroid'
import { FontKeyType, ThemaKeyType } from '@/types'
import { useSession } from 'next-auth/react'
import { useState } from 'react'
import { useBoard } from '@/app/board/[boardId]/_contexts/BoardContext'
import { uploadAction } from '../../_actions/uploadAction'
import { useModal } from './ModalContext'

interface CreatePolaroidProps {
id: string
}

const CreatePolaroidModal = ({ id }: CreatePolaroidProps) => {
const CreatePolaroidModal = () => {
const { boardId: id } = useBoard()
const [isValid, setIsValid] = useState<boolean>(false)
const [image, setImage] = useState<File | null>(null)
const [nickname, setNickname] = useState<string>('')
Expand Down
42 changes: 42 additions & 0 deletions src/app/board/[boardId]/_components/Header/DefaultHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client'

import React from 'react'
import Header from '@/components/Header'
import PinIcon from 'public/icons/pinFilled.svg'
import Hamburger from '@/components/HamburgerMenu'
import Tutorial from '@/app/board/[boardId]/_components/Tutorial'
import { Step1Tooltip } from '@/app/board/[boardId]/_components/Tutorial/Tooltips'
import ShareBtn from '@/app/board/[boardId]/_components/Share'
import { useSession } from 'next-auth/react'
import { useBoard } from '@/app/board/[boardId]/_contexts/BoardContext'
import { twMerge } from 'tailwind-merge'

const DefaultHeader = ({ className }: { className: string }) => {
const { data: session } = useSession()
const { board } = useBoard()

return (
<Header
title={
<div className="flex items-center justify-center gap-[3px] text-center">
<PinIcon />
<h1 className="text-md font-semiBold leading-6">{board.title}</h1>
</div>
}
leftButton={<Hamburger />}
rightButton={
session ? (
<Tutorial step={1} tooltip={<Step1Tooltip />} hasNext>
<ShareBtn />
</Tutorial>
) : (
<ShareBtn />
)
}
className={twMerge('bg-transparent', className)}
shadow={false}
/>
)
}

export default DefaultHeader
30 changes: 30 additions & 0 deletions src/app/board/[boardId]/_components/Header/SelectModeHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react'
import Header from '@/components/Header'
import { useSelect } from '@/app/board/[boardId]/_contexts/SelectModeContext'
import { useBoard } from '@/app/board/[boardId]/_contexts/BoardContext'
import { twMerge } from 'tailwind-merge'

const SelectModeHeader = ({ className }: { className: string }) => {
const { selectedIds, MAX_SELECT_COUNT } = useSelect()
const { board } = useBoard()
const maxLength = Math.min(board.items.length, MAX_SELECT_COUNT)

return (
<Header
title={
<div className="flex flex-col items-center justify-center gap-[3px] text-center text-md font-semiBold leading-6">
<h1>꾸미고 싶은 폴라로이드를 골라주세요</h1>
<h2 className="flex gap-0.5">
<span className="text-gray-400">{selectedIds.length}</span>
<span className="text-gray-400">/</span>
<span>{maxLength}</span>
</h2>
</div>
}
className={twMerge('bg-transparent', className)}
shadow={false}
/>
)
}

export default SelectModeHeader
22 changes: 22 additions & 0 deletions src/app/board/[boardId]/_components/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client'

import React from 'react'
import { useSelect } from '@/app/board/[boardId]/_contexts/SelectModeContext'
import DefaultHeader from '@/app/board/[boardId]/_components/Header/DefaultHeader'
import SelectModeHeader from '@/app/board/[boardId]/_components/Header/SelectModeHeader'
import { useBoard } from '@/app/board/[boardId]/_contexts/BoardContext'
import { BOARDTHEMAS } from '@/lib/constants/boardConfig'

const Header = () => {
const { isSelectMode } = useSelect()
const { board } = useBoard()
const boardTheme = BOARDTHEMAS[board.options.THEMA].theme
const className = boardTheme === 'LIGHT' ? 'text-gray-900' : 'text-gray-0'

if (isSelectMode) {
return <SelectModeHeader className={className} />
}
return <DefaultHeader className={className} />
}

export default Header
13 changes: 10 additions & 3 deletions src/app/board/[boardId]/_components/OpenModalBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,25 @@ import Modal from '@/components/Modal'
import { useSession } from 'next-auth/react'
import AddPolaroid from 'public/icons/add_polaroid.svg'
import { ReactNode } from 'react'
import { useBoard } from '@/app/board/[boardId]/_contexts/BoardContext'
import { useSelect } from '@/app/board/[boardId]/_contexts/SelectModeContext'
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 OpenModalBtn = ({ children }: OpenModalBtnProps) => {
const { data: session } = useSession()
const { isOpen, openModal, closeModal } = useModal()
const { board } = useBoard()
const { isSelectMode } = useSelect()

const renderModalContent = () => {
if (polaroidNum >= 30) {
if (board?.items.length >= 30) {
return <CannotUploadModal isOpen={isOpen} onClose={closeModal} />
}
return (
Expand All @@ -29,6 +32,10 @@ const OpenModalBtn = ({ polaroidNum, children }: OpenModalBtnProps) => {
)
}

if (isSelectMode) {
return null
}

return (
<div>
{isOpen && renderModalContent()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import PolaroidCard from '@/components/Polaroid/PolaroidCard'

interface PolaroidListItemProps {
item: Polaroid
PolaroidCardClassName?: string
onClick: () => void
}

const PolaroidListItem = ({ item, onClick }: PolaroidListItemProps) => {
const PolaroidListItem = ({
item,
onClick,
PolaroidCardClassName = '',
}: PolaroidListItemProps) => {
const [rotate, setRotate] = useState<number>(0)
useEffect(() => {
const randomRotate =
Expand All @@ -20,7 +25,11 @@ const PolaroidListItem = ({ item, onClick }: PolaroidListItemProps) => {
className="flex flex-col justify-center"
style={{ rotate: `${rotate}deg` }}
>
<PolaroidCard polaroid={item} onClick={onClick} />
<PolaroidCard
className={PolaroidCardClassName}
polaroid={item}
onClick={onClick}
/>
</div>
)
}
Expand Down
110 changes: 83 additions & 27 deletions src/app/board/[boardId]/_components/PolaroidList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,100 @@
'use client'

import React, { useState } from 'react'
import PolaroidDetailModal from '@/components/Polaroid/PolaroidDetail'
import { Board } from '@/types'
import { useState } from 'react'
import PolaroidListItem from './PolaroidListItem'
import { useBoard } from '@/app/board/[boardId]/_contexts/BoardContext'
import { useSelect } from '@/app/board/[boardId]/_contexts/SelectModeContext'
import Button from '@/components/Button'
import { useRouter } from 'next/navigation'
import PolaroidListItem from '@/app/board/[boardId]/_components/PolaroidList/PolaroidListItem'

interface PolaroidListProps {
board: Board
boardId: string
}

const PolaroidList = ({ boardId, board }: PolaroidListProps) => {
const PolaroidList = () => {
const { board, boardId } = useBoard()
const { isSelectMode, selectedIds, toggleSelectedId } = useSelect()
const [isModalOpen, setIsModalOpen] = useState(false)
const [selectedIdx, setSelectedIdx] = useState<number>(0)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()

const openDetailModal = (idx: number) => {
setSelectedIdx(idx)
setIsModalOpen(true)
}

const selectPolaroid = (idx: number) => {
const { id } = board.items[idx]
toggleSelectedId(id)
}

const onSelectPolaroid = isSelectMode ? selectPolaroid : openDetailModal

const getPolaroidClassName = (idx: number) => {
const { id } = board.items[idx]

if (isSelectMode && selectedIds.includes(id)) {
return 'border-2 border-negative'
}

return ''
}

const onSelectComplete = async () => {
setIsLoading(true)
const res = await fetch(`/board/api/screenshot`, {
method: 'POST',
body: JSON.stringify({
polaroids: selectedIds,
boardId,
}),
})

if (!res.ok) {
throw new Error('Failed to take screenshot')
}

const blob = await res.blob()
const imageUrl = URL.createObjectURL(blob)
setIsLoading(false)

router.push(`/board/${boardId}/decorate?imageUrl=${imageUrl}`)
}

return (
<div className="mx-auto w-full flex-1 overflow-x-hidden overflow-y-scroll pb-10 scrollbar-hide">
<div className="grid grid-cols-2 gap-6 px-[20px] py-[10px]">
{board.items.map((item, idx) => (
<PolaroidListItem
key={item.id}
item={item}
onClick={() => openDetailModal(idx)}
/>
))}
<>
<div className="mx-auto w-full flex-1 overflow-x-hidden overflow-y-scroll pb-10 scrollbar-hide">
<div className="grid grid-cols-2 gap-6 px-[20px] py-[10px]">
{board.items.map((item, idx) => (
<PolaroidListItem
PolaroidCardClassName={getPolaroidClassName(idx)}
key={item.id}
item={item}
onClick={() => onSelectPolaroid(idx)}
/>
))}
</div>
</div>
<PolaroidDetailModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
selectedIdx={selectedIdx}
polaroids={board.items}
boardId={boardId}
isBoardOwner={board.mine}
/>
</div>
{isSelectMode ? (
<div className="absolute bottom-10 mx-10 flex w-[calc(100%-80px)] justify-center">
<Button
size="lg"
className="w-full"
disabled={selectedIds.length === 0 || isLoading}
onClick={onSelectComplete}
>
선택 완료
</Button>
</div>
) : (
<PolaroidDetailModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
selectedIdx={selectedIdx}
polaroids={board.items}
boardId={boardId}
isBoardOwner={board.mine}
/>
)}
</>
)
}

Expand Down
17 changes: 17 additions & 0 deletions src/app/board/[boardId]/_components/Share/CopyCompleteToast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react'
import Toast from '@/components/Toast'

interface CopyCompleteToastProps {
show: boolean
close: () => void
}

const CopyCompleteToast = ({ show, close }: CopyCompleteToastProps) => {
return (
<Toast isOpen={show} setClose={close}>
클립보드에 링크가 복사되었어요!
</Toast>
)
}

export default CopyCompleteToast
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react'
import Toast from '@/components/Toast'

interface CopyCompleteToastProps {
show: boolean
close: () => void
}

const CopyCompleteToast = ({ show, close }: CopyCompleteToastProps) => {
return (
<Toast isOpen={show} setClose={close}>
<div className="">
<div>
보드를 꾸미기 위해 <span className="font-semiBold">1장 이상</span>
</div>
<div>폴라로이드를 업로드 해주세요!</div>
</div>
</Toast>
)
}

export default CopyCompleteToast
10 changes: 3 additions & 7 deletions src/app/board/[boardId]/_components/Share/Section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@ import { ComponentProps, MouseEventHandler, ReactNode } from 'react'
import { twMerge } from 'tailwind-merge'

interface SectionProps {
title: string
children: ReactNode
}

const Section = ({ title, children }: SectionProps) => (
<div className="flex w-full flex-col border-b-[1px] border-gray-200 pb-6 pt-5">
<h2 className="pb-5 pl-5 text-sm">{title}</h2>
<div className="flex gap-4 overflow-x-scroll px-5 scrollbar-hide">
{children}
</div>
const Section = ({ children }: SectionProps) => (
<div className="flex w-full flex-row justify-evenly pb-6 pt-5">
{children}
</div>
)

Expand Down
Loading

0 comments on commit d39260a

Please sign in to comment.