Skip to content

Commit 22fc450

Browse files
committed
WIP: handle item modal
1 parent 8caccdc commit 22fc450

File tree

13 files changed

+221
-61
lines changed

13 files changed

+221
-61
lines changed

Diff for: .gitignore

+1-5
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,4 @@
2323
**/.turbo
2424
out
2525
# Turborepo
26-
.turbo
27-
28-
29-
# Claude
30-
.gpt-runner
26+
.turbo

Diff for: apps/book-web/src/layouts/NextraLayout/NextraLayout.tsx

+8-6
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ function NextraLayout({ children, mdxText }: Props) {
7070
}, [getPageData, isGetPageLoading])
7171

7272
useEffect(() => {
73-
const addAction = async (e: CustomEventInit<CustomEventDetail['addActionEvent']>) => {
73+
const createOrUpdate = async (
74+
e: CustomEventInit<CustomEventDetail['createOrUpdateItemEvent']>,
75+
) => {
7476
if (!e.detail) return
7577
if (isReorderPagePending) return
7678
const { title, parentUrlSlug, index, bookUrlSlug, type } = e.detail
@@ -85,14 +87,14 @@ function NextraLayout({ children, mdxText }: Props) {
8587
})
8688
getPagesRefetch()
8789
}
88-
window.addEventListener(nextraCustomEventName.addActionEvent, addAction)
90+
window.addEventListener(nextraCustomEventName.createOrUpdateItemEvent, createOrUpdate)
8991
return () => {
90-
window.removeEventListener(nextraCustomEventName.addActionEvent, addAction)
92+
window.removeEventListener(nextraCustomEventName.createOrUpdateItemEvent, createOrUpdate)
9193
}
9294
})
9395

9496
useEffect(() => {
95-
const changeItem = async (e: CustomEventInit<CustomEventDetail['changeItemEvent']>) => {
97+
const changeItem = async (e: CustomEventInit<CustomEventDetail['changeItemOrderEvent']>) => {
9698
if (!e.detail) return
9799
if (isCreatePagePending) return
98100

@@ -109,9 +111,9 @@ function NextraLayout({ children, mdxText }: Props) {
109111
getPagesRefetch()
110112
}
111113

112-
window.addEventListener(nextraCustomEventName.changeItemEvent, changeItem)
114+
window.addEventListener(nextraCustomEventName.changeItemOrderEvent, changeItem)
113115
return () => {
114-
window.removeEventListener(nextraCustomEventName.changeItemEvent, changeItem)
116+
window.removeEventListener(nextraCustomEventName.changeItemOrderEvent, changeItem)
115117
}
116118
}, [])
117119

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react'
2+
import ModalWrapper from './modal-wrapper'
3+
import { useModal } from '../../contexts/modal'
4+
5+
const ItemDelete: React.FC = () => {
6+
const { modalType, isModalOpen, setIsModalOpen } = useModal()
7+
8+
const handleDeleteClick = () => {
9+
setIsModalOpen(true)
10+
}
11+
12+
const handleCloseModal = () => {
13+
setIsModalOpen(false)
14+
}
15+
16+
const handleConfirmDelete = () => {
17+
// 여기서 삭제 로직을 추가하세요
18+
console.log('Item deleted')
19+
setIsModalOpen(false)
20+
}
21+
22+
if (modalType !== 'deleteItem') return null
23+
return (
24+
<div>
25+
<button className="nx-btn nx-btn-danger" onClick={handleDeleteClick}>
26+
삭제
27+
</button>
28+
29+
<ModalWrapper isOpen={isModalOpen} onClose={handleCloseModal}>
30+
<h2 className="nx-mb-4">정말 삭제하시겠습니까?</h2>
31+
<div className="nx-flex nx-justify-end nx-gap-4">
32+
<button className="nx-btn nx-btn-primary" onClick={handleConfirmDelete}>
33+
확인
34+
</button>
35+
<button className="nx-btn nx-btn-secondary" onClick={handleCloseModal}>
36+
취소
37+
</button>
38+
</div>
39+
</ModalWrapper>
40+
</div>
41+
)
42+
}
43+
44+
export default ItemDelete
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react'
2+
3+
interface ModalShadowProps {
4+
isOpen: boolean
5+
onClose: () => void
6+
}
7+
8+
const ModalShadow: React.FC<ModalShadowProps> = ({ isOpen, onClose }) => {
9+
if (!isOpen) return null
10+
return <div className="nx-fixed nx-inset-0 nx-bg-black nx-opacity-50" onClick={onClose}></div>
11+
}
12+
13+
export default ModalShadow
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React, { useEffect, useState } from 'react'
2+
import ModalShadow from './modal-shadow'
3+
4+
interface ModalWrapperProps {
5+
isOpen: boolean
6+
onClose: () => void
7+
children: React.ReactNode
8+
}
9+
10+
const ModalWrapper: React.FC<ModalWrapperProps> = ({ isOpen, onClose, children }) => {
11+
const [isVisible, setIsVisible] = useState(isOpen)
12+
13+
useEffect(() => {
14+
if (isOpen) {
15+
setIsVisible(true)
16+
} else {
17+
setTimeout(() => setIsVisible(false), 300) // 애니메이션 시간이 300ms라고 가정
18+
}
19+
}, [isOpen])
20+
21+
if (!isVisible) return null
22+
23+
return (
24+
<div className="nx-fixed nx-inset-0 nx-z-50 nx-flex nx-items-center nx-justify-center">
25+
<ModalShadow isOpen={isOpen} onClose={onClose} />
26+
<div
27+
className={`nx-z-50 nx-transform nx-rounded nx-bg-white nx-p-8 nx-shadow-lg nx-transition-transform ${isOpen ? 'nx-translate-y-0' : 'nx-translate-y-full'}`}
28+
>
29+
{children}
30+
</div>
31+
</div>
32+
)
33+
}
34+
35+
export default ModalWrapper

Diff for: packages/nextra-editor/src/components/sidebar/sidebar-controller/control-input.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ function ControlInput({ type }: Props): ReactElement {
7676
if (type === '') return
7777
if (!title) return
7878
if (error) return
79-
const event = new CustomEvent<CustomEventDetail['addActionEvent']>(
80-
nextraCustomEventName.addActionEvent,
79+
const event = new CustomEvent<CustomEventDetail['createOrUpdateItemEvent']>(
80+
nextraCustomEventName.createOrUpdateItemEvent,
8181
{
8282
detail: { title, parentUrlSlug, index, bookUrlSlug, type },
8383
},

Diff for: packages/nextra-editor/src/components/sidebar/sidebar-controller/control-menu.tsx

+29-25
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { forwardRef } from 'react'
66
type Props = {
77
isOpen: boolean
88
position: { top: number; left: number }
9+
onEdit: () => void
10+
onDelete: () => void
911
}
1012

1113
const style = {
@@ -16,30 +18,32 @@ const style = {
1618
svg: cn('nx-mr-2'),
1719
}
1820

19-
const ControlMenu = forwardRef<HTMLDivElement, Props>(({ isOpen, position }, ref) => {
20-
return (
21-
<div
22-
ref={ref}
23-
className={cn(
24-
'nx-absolute nx-z-20 nx-rounded-md nx-py-2 nx-shadow-lg',
25-
'nx-text-sm',
26-
'nx-bg-white nx-text-gray-600',
27-
'dark:nx-bg-neutral-800 dark:nx-text-gray-300',
28-
)}
29-
style={{ top: position.top, left: 260, display: isOpen ? 'block' : 'none' }}
30-
>
31-
<ul>
32-
<li className={style.list}>
33-
<EditIcon className={style.svg} />
34-
<span>이름 바꾸기</span>
35-
</li>
36-
<li className={style.list}>
37-
<TrashIcon className={style.svg} />
38-
<span>삭제</span>
39-
</li>
40-
</ul>
41-
</div>
42-
)
43-
})
21+
const ControlMenu = forwardRef<HTMLDivElement, Props>(
22+
({ isOpen, position, onEdit, onDelete }, ref) => {
23+
return (
24+
<div
25+
ref={ref}
26+
className={cn(
27+
'nx-absolute nx-z-20 nx-rounded-md nx-py-2 nx-shadow-lg',
28+
'nx-text-sm',
29+
'nx-bg-white nx-text-gray-600',
30+
'dark:nx-bg-neutral-800 dark:nx-text-gray-300',
31+
)}
32+
style={{ top: position.top, left: 260, display: isOpen ? 'block' : 'none' }}
33+
>
34+
<ul>
35+
<li className={style.list} onClick={onEdit}>
36+
<EditIcon className={style.svg} />
37+
<span>이름 바꾸기</span>
38+
</li>
39+
<li className={style.list} onClick={onDelete}>
40+
<TrashIcon className={style.svg} />
41+
<span>삭제</span>
42+
</li>
43+
</ul>
44+
</div>
45+
)
46+
},
47+
)
4448

4549
export default ControlMenu

Diff for: packages/nextra-editor/src/components/sidebar/sortable-tree/index.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export const useDndTree = () => useContext(DndTreeContext)
7272

7373
function SortableTree({ items, sidebarRef, showSidebar, onItemsChanged }: Props) {
7474
const { bookUrlSlug } = useUrlSlug()
75-
const { isFolding, setCollapsedTree } = useSidebar()
75+
const { isFolding, setCollapsedTree, actionActive } = useSidebar()
7676
const [isDragging, setDragging] = useState(false)
7777
const [ghostItem, setGhostItem] = useState<SortableItem | null>(null)
7878
const [overItem, setOverItem] = useState<SortableItem | null>(null)
@@ -206,7 +206,7 @@ function SortableTree({ items, sidebarRef, showSidebar, onItemsChanged }: Props)
206206
const onDragEnd = ({ active, over }: DragEndEvent) => {
207207
resetState()
208208

209-
if (projected && over) {
209+
if (projected && over && !actionActive) {
210210
const { depth, parentId } = projected
211211
const clonedItems: FlattenedItem[] = JSON.parse(JSON.stringify(flattenTree(items)))
212212
const overIndex = clonedItems.findIndex(({ id }) => id === over.id)
@@ -227,8 +227,8 @@ function SortableTree({ items, sidebarRef, showSidebar, onItemsChanged }: Props)
227227
const newItems = buildTree(sortedItems)
228228
const newParentItem = findItemDeep(newItems, parentId)
229229

230-
const event = new CustomEvent<CustomEventDetail['changeItemEvent']>(
231-
nextraCustomEventName.changeItemEvent,
230+
const event = new CustomEvent<CustomEventDetail['changeItemOrderEvent']>(
231+
nextraCustomEventName.changeItemOrderEvent,
232232
{
233233
detail: {
234234
bookUrlSlug,

Diff for: packages/nextra-editor/src/components/sidebar/sortable-tree/sortable-item.tsx

+26-12
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,18 @@ export const SortableItem = forwardRef<HTMLDivElement, SortableItemProps>((props
3737
} = props
3838

3939
const isControlAction = ['newPage', 'newFolder', 'newSeparator'].includes(item.type)
40-
4140
const active =
4241
!isDragging && !isControlAction && !isGhost && [route, `${route}/`].includes(item.route + '/')
4342
// const isLink = 'withIndexPage' in item && item.withIndexPage
4443

4544
const isShowMenu = showMenuId === item.id
45+
const isSeparator = item.type === 'separator'
46+
47+
const actionMap: Record<string, ActionType> = {
48+
newPage: 'page',
49+
newFolder: 'folder',
50+
newSeparator: 'separator',
51+
}
4652

4753
useEffect(() => {
4854
if (isGhost) return
@@ -67,7 +73,14 @@ export const SortableItem = forwardRef<HTMLDivElement, SortableItemProps>((props
6773
setShowMenuId(null)
6874
}
6975

70-
const isSeparator = item.type === 'separator'
76+
const onDelete = () => {
77+
console.log('delete')
78+
}
79+
80+
const onEdit = () => {
81+
onCloseMenu()
82+
setIsEdit(!isEdit)
83+
}
7184

7285
const wrapperStyle: CSSProperties = {
7386
listStyle: 'none',
@@ -80,12 +93,6 @@ export const SortableItem = forwardRef<HTMLDivElement, SortableItemProps>((props
8093
Object.assign(wrapperStyle, style)
8194
}
8295

83-
const addActionMap: Record<string, ActionType> = {
84-
newPage: 'page',
85-
newFolder: 'folder',
86-
newSeparator: 'separator',
87-
}
88-
8996
const { ref: menuRef } = useOutsideClick<HTMLDivElement>(onCloseMenu)
9097

9198
return (
@@ -97,7 +104,13 @@ export const SortableItem = forwardRef<HTMLDivElement, SortableItemProps>((props
97104
onContextMenu={onOpenMenu}
98105
>
99106
{createPortal(
100-
<ControlMenu ref={menuRef} isOpen={showMenuId === item.id} position={mousePosition} />,
107+
<ControlMenu
108+
ref={menuRef}
109+
isOpen={showMenuId === item.id}
110+
position={mousePosition}
111+
onEdit={onEdit}
112+
onDelete={onDelete}
113+
/>,
101114
document.getElementById('menu-root')!,
102115
)}
103116
<div
@@ -107,7 +120,8 @@ export const SortableItem = forwardRef<HTMLDivElement, SortableItemProps>((props
107120
'nx-flex nx-w-full nx-items-center nx-justify-between nx-gap-2 nx-text-left',
108121
isSeparator && 'nx-cursor-default',
109122
isSeparator ? classes.separator : classes.link,
110-
active ? classes.active : classes.inactive,
123+
!isControlAction && active && classes.active,
124+
!isControlAction && !active && classes.inactive,
111125
!isControlAction && !isDragging && !active && classes.inactiveBgColor,
112126
isGhost && classes.ghost,
113127
clone && classes.clone,
@@ -130,8 +144,8 @@ export const SortableItem = forwardRef<HTMLDivElement, SortableItemProps>((props
130144
router.push(item.route, item.route, { shallow: true })
131145
}}
132146
>
133-
{isControlAction ? (
134-
<ControlInput type={addActionMap[item.type]} />
147+
{isControlAction || isEdit ? (
148+
<ControlInput type={actionMap[item.type]} />
135149
) : (
136150
<>
137151
<div className={cn('nx-w-full nx-px-2 nx-py-1.5 [word-break:keep-all]')}>

Diff for: packages/nextra-editor/src/contexts/modal.tsx

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import * as React from 'react'
2+
import { createContext, ReactNode, useContext, useState } from 'react'
3+
4+
type ModalType = 'deleteItem'
5+
6+
interface Modal {
7+
isModalOpen: boolean
8+
setIsModalOpen: (value: boolean) => void
9+
modalType: ModalType | null
10+
setModalType: (value: ModalType | null) => void
11+
reset: () => void
12+
}
13+
14+
const ModalContext = createContext<Modal>({
15+
isModalOpen: false,
16+
setIsModalOpen: () => {},
17+
modalType: null,
18+
setModalType: () => {},
19+
reset: () => {},
20+
})
21+
22+
export function useModal() {
23+
return useContext(ModalContext)
24+
}
25+
26+
export const ModalProvider = ({ children }: { children: ReactNode }) => {
27+
const [isModalOpen, setIsModalOpen] = useState<boolean>(false)
28+
const [modalType, setModalType] = useState<ModalType | null>(null)
29+
30+
const reset = () => {
31+
setIsModalOpen(false)
32+
setModalType(null)
33+
}
34+
35+
const value: Modal = {
36+
isModalOpen,
37+
setIsModalOpen,
38+
modalType,
39+
setModalType,
40+
reset,
41+
}
42+
43+
return <ModalContext.Provider value={value}>{children}</ModalContext.Provider>
44+
}

Diff for: packages/nextra-editor/src/index.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -237,11 +237,12 @@ export {
237237
} from './components'
238238

239239
export const nextraCustomEventName: Record<keyof CustomEventDetail, string> = {
240-
addActionEvent: 'addActionEvent',
241-
changeItemEvent: 'changeItemEvent',
240+
createOrUpdateItemEvent: 'createOrUpdateItemEvent',
241+
changeItemOrderEvent: 'changeItemOrderEvent',
242242
saveItemBodyEvent: 'saveItemBodyEvent',
243243
deployStartEvent: 'deployStartEvent',
244244
deployEndEvent: 'deployEndEvent',
245+
deleteItemEvent: 'deleteItemEvent',
245246
}
246247

247248
export { CustomEventDetail, MdxCompilerOptions, MdxOptions, SearchResult }

0 commit comments

Comments
 (0)