Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 26 additions & 22 deletions src/features/create-post/ui/CreateModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ function CreateModalInner({
<DialogContent
showCloseButton={false}
className={[
'flex flex-col gap-0 overflow-hidden rounded-4xl bg-white p-0 transition-[max-width] duration-300',
'flex flex-col gap-0 overflow-hidden rounded-4xl bg-white p-0 transition-[width,max-width] duration-300 sm:h-auto sm:max-w-[calc(100vw-2rem)]',
isDetails
? 'sm:h-[min(80vh,560px)] sm:max-w-4xl'
: 'aspect-square sm:h-[min(80vh,560px)] sm:max-w-xl',
? 'sm:w-[calc(80vh-51px+340px)] sm:max-w-[849px]'
: 'sm:w-[calc(80vh-51px)] sm:max-w-[509px]',
].join(' ')}
>
<CreateModalHeader
Expand All @@ -89,7 +89,7 @@ function CreateModalInner({
{isUploaded ? (
isDetails ? (
<div className="flex min-h-0 flex-1 flex-col sm:flex-row">
<div className="flex min-h-0 flex-1 sm:w-[560px] sm:shrink-0">
<div className="flex aspect-square w-full sm:h-[calc(80vh-51px)] sm:max-h-[509px] sm:w-[calc(80vh-51px)] sm:max-w-[509px] sm:flex-none">
<PreviewPane
activePreviewUrl={activePreviewUrl}
filesCount={files.length}
Expand All @@ -107,26 +107,30 @@ function CreateModalInner({
</div>
</div>
) : (
<PreviewPane
activePreviewUrl={activePreviewUrl}
filesCount={files.length}
activeIndex={carousel.activeIndex}
canGoPrev={carousel.canGoPrev}
canGoNext={carousel.canGoNext}
dots={carousel.dots}
goPrev={carousel.goPrev}
goNext={carousel.goNext}
/>
<div className="flex aspect-square w-full sm:h-[calc(80vh-51px)] sm:max-h-[509px] sm:w-[calc(80vh-51px)] sm:max-w-[509px]">
<PreviewPane
activePreviewUrl={activePreviewUrl}
filesCount={files.length}
activeIndex={carousel.activeIndex}
canGoPrev={carousel.canGoPrev}
canGoNext={carousel.canGoNext}
dots={carousel.dots}
goPrev={carousel.goPrev}
goNext={carousel.goNext}
/>
</div>
)
) : (
<Dropzone
accept={CREATE_POST_IMAGE_ACCEPT}
multiple
maxSizeBytes={MAX_IMAGE_FILE_SIZE_BYTES}
onDropFiles={handleDropFiles}
>
{(api) => <EmptyDropzoneState {...api} />}
</Dropzone>
<div className="flex aspect-square w-full sm:h-[calc(80vh-51px)] sm:max-h-[509px] sm:w-[calc(80vh-51px)] sm:max-w-[509px]">
<Dropzone
accept={CREATE_POST_IMAGE_ACCEPT}
multiple
maxSizeBytes={MAX_IMAGE_FILE_SIZE_BYTES}
onDropFiles={handleDropFiles}
>
{(api) => <EmptyDropzoneState {...api} />}
</Dropzone>
</div>
)}
</DialogContent>
</Dialog>
Expand Down
2 changes: 1 addition & 1 deletion src/features/create-post/ui/EmptyDropzoneState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function EmptyDropzoneState({
>
<ImagePlus className="size-10" />

<p className="text-lg font-medium">
<p className="text-lg font-medium break-keep">
사진과 동영상을 여기에 끌어다 놓으세요
</p>

Expand Down
5 changes: 5 additions & 0 deletions src/features/create-story/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const MAX_STORY_IMAGE_FILES = 1

export const MAX_STORY_IMAGE_FILE_SIZE_BYTES = 10 * 1024 * 1024

export const CREATE_STORY_IMAGE_ACCEPT = 'image/*'
40 changes: 40 additions & 0 deletions src/features/create-story/model/hooks/useCreateStoryDraft.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useCallback, useMemo, useState } from 'react'

import { useObjectUrl } from '@/features/create-post/model/hooks/useObjectUrl'
import { useLimitedFilesDrop } from '@/features/create-post/model/hooks/useLimitedFilesDrop'

type UseCreateStoryDraftParams = {
maxFiles: number
onIgnoredCount?: (ignoredCount: number) => void
}

export function useCreateStoryDraft({
maxFiles,
onIgnoredCount,
}: UseCreateStoryDraftParams) {
const [file, setFile] = useState<File | null>(null)

const isUploaded = useMemo(() => file !== null, [file])

const resetDraft = useCallback(() => {
setFile(null)
}, [])

const handleDropFiles = useLimitedFilesDrop({
maxFiles,
onAcceptedFiles: (limited) => {
setFile(limited[0] ?? null)
},
onIgnoredCount,
})

const previewUrl = useObjectUrl(file)

return {
file,
isUploaded,
previewUrl,
handleDropFiles,
resetDraft,
}
}
101 changes: 101 additions & 0 deletions src/features/create-story/ui/CreateStoryModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { useCallback } from 'react'

import { Dialog, DialogContent } from '@/shared/ui/dialog'
import { Dropzone } from '@/shared/ui/dropzone'
import { toast } from 'sonner'

import {
CREATE_STORY_IMAGE_ACCEPT,
MAX_STORY_IMAGE_FILES,
MAX_STORY_IMAGE_FILE_SIZE_BYTES,
} from '@/features/create-story/constants'
import { useCreateStoryDraft } from '@/features/create-story/model/hooks/useCreateStoryDraft'
import { useDiscardConfirm } from '@/features/create-post/model/hooks/useDiscardConfirm'
import { StoryModalHeader } from '@/features/create-story/ui/StoryModalHeader'
import { DiscardStoryDialog } from '@/features/create-story/ui/DiscardStoryDialog'
import { EmptyStoryDropzoneState } from '@/features/create-story/ui/EmptyStoryDropzoneState'
import { StoryPreviewPane } from '@/features/create-story/ui/StoryPreviewPane'

type CreateStoryModalProps = {
open: boolean
onOpenChange: (open: boolean) => void
}

export function CreateStoryModal({
open,
onOpenChange,
}: CreateStoryModalProps) {
if (!open) return null
return <CreateStoryModalInner onOpenChange={onOpenChange} />
}

function CreateStoryModalInner({
onOpenChange,
}: Pick<CreateStoryModalProps, 'onOpenChange'>) {
const { isUploaded, previewUrl, handleDropFiles, resetDraft } =
useCreateStoryDraft({
maxFiles: MAX_STORY_IMAGE_FILES,
onIgnoredCount: (ignoredCount) => {
if (ignoredCount <= 0) return
toast('스토리에는 사진 1장만 업로드할 수 있습니다.')
},
})

const closeWithoutConfirm = useCallback(() => {
resetDraft()
onOpenChange(false)
}, [onOpenChange, resetDraft])

const {
isConfirmOpen,
setConfirmOpen,
requestClose,
handleDialogOpenChange,
} = useDiscardConfirm({
isDirty: isUploaded,
onClose: closeWithoutConfirm,
})

return (
<>
<Dialog open onOpenChange={handleDialogOpenChange}>
<DialogContent
showCloseButton={false}
className="flex max-h-[calc(100dvh-2rem)] flex-col gap-0 overflow-hidden rounded-4xl bg-white p-0 transition-[width,max-width] duration-300 sm:w-[calc((80vh-51px)*9/16)] sm:max-w-[calc(100vw-2rem)]"
>
<StoryModalHeader
isUploaded={isUploaded}
onBack={requestClose}
onShare={() => toast('스토리 공유하기')}
/>
<div className="h-px w-full bg-zinc-200" />

<div className="flex min-h-0 flex-1 items-center justify-center overflow-hidden">
<div className="relative aspect-9/16 max-h-full min-h-0 w-full max-w-full">
{isUploaded ? (
<StoryPreviewPane previewUrl={previewUrl} />
) : (
<div className="flex h-full w-full items-center justify-center">
<Dropzone
accept={CREATE_STORY_IMAGE_ACCEPT}
multiple={false}
maxSizeBytes={MAX_STORY_IMAGE_FILE_SIZE_BYTES}
onDropFiles={handleDropFiles}
>
{(api) => <EmptyStoryDropzoneState {...api} />}
</Dropzone>
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>

<DiscardStoryDialog
open={isConfirmOpen}
onOpenChange={setConfirmOpen}
onDiscard={closeWithoutConfirm}
/>
</>
)
}
57 changes: 57 additions & 0 deletions src/features/create-story/ui/DiscardStoryDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from '@/shared/ui/dialog'

type DiscardStoryDialogProps = {
open: boolean
onOpenChange: (open: boolean) => void
onDiscard: () => void
}

export function DiscardStoryDialog({
open,
onOpenChange,
onDiscard,
}: DiscardStoryDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
showCloseButton={false}
overlayClassName="z-[60]"
className="z-60 w-[min(92vw,520px)] gap-0 overflow-hidden rounded-3xl bg-white p-0"
>
<div className="px-8 pt-10 pb-8 text-center">
<DialogTitle className="text-2xl leading-tight font-semibold">
스토리를 삭제하시겠어요?
</DialogTitle>
<DialogDescription className="mt-2 text-sm text-zinc-500">
지금 나가면 수정 내용이 저장되지 않습니다.
</DialogDescription>
</div>

<div className="h-px w-full bg-zinc-200" />

<button
type="button"
className="w-full px-6 py-5 text-center text-base font-semibold text-red-500 hover:bg-zinc-50"
onClick={onDiscard}
>
삭제
</button>

<div className="h-px w-full bg-zinc-200" />

<button
type="button"
className="w-full px-6 py-5 text-center text-base font-medium hover:bg-zinc-50"
onClick={() => onOpenChange(false)}
>
취소
</button>
</DialogContent>
</Dialog>
)
}
53 changes: 53 additions & 0 deletions src/features/create-story/ui/EmptyStoryDropzoneState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { ComponentPropsWithRef, HTMLAttributes } from 'react'

import { Button } from '@/shared/ui/button'
import { ImagePlus } from 'lucide-react'

type EmptyStoryDropzoneStateProps = {
getRootProps: (
props?: HTMLAttributes<HTMLElement>
) => HTMLAttributes<HTMLElement>
getInputProps: (
props?: ComponentPropsWithRef<'input'>
) => ComponentPropsWithRef<'input'>
openFileDialog: () => void
isDragging: boolean
}

export function EmptyStoryDropzoneState({
getRootProps,
getInputProps,
openFileDialog,
isDragging,
}: EmptyStoryDropzoneStateProps) {
return (
<div
{...getRootProps({
className: [
'flex flex-1 flex-col items-center justify-center gap-4 px-6 py-12 text-center',
isDragging ? 'bg-zinc-50' : '',
].join(' '),
})}
>
<ImagePlus className="size-10" />

<p className="text-lg font-medium break-keep">
스토리에 올릴 사진을 여기에 끌어다 놓으세요
</p>

<input {...getInputProps({ className: 'hidden' })} />

<Button
type="button"
variant="outline"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
openFileDialog()
}}
>
컴퓨터에서 선택
</Button>
</div>
)
}
Loading