Skip to content

Commit 85a4ecd

Browse files
committed
feat: drag and drop image upload
1 parent e76d00c commit 85a4ecd

File tree

12 files changed

+153
-27
lines changed

12 files changed

+153
-27
lines changed

apps/book-web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"lint": "next lint",
1111
"codegen": "pnpm env:copy -e development && graphql-codegen-esm --config codegen.ts -r dotenv/config",
1212
"env:copy": "tsx ./scripts/copyEnv.mts",
13-
"create-component": "tsx ./scripts/createComponent.ts",
13+
"create-component": "tsx ./scripts/createComponent.mts",
1414
"ssm": "tsx ./scripts/ssm.mts"
1515
},
1616
"dependencies": {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.block {
2+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import ErrorBoundary from './ErrorBoundary'
2+
import { render } from '@testing-library/react'
3+
4+
describe('ErrorBoundary', () => {
5+
it('renders successfully', () => {
6+
render(<ErrorBoundary />)
7+
})
8+
})
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { ErrorBoundaryProps } from 'next/dist/client/components/error-boundary'
2+
import React from 'react'
3+
4+
type State = { hasError: boolean }
5+
6+
class ErrorBoundary extends React.Component<ErrorBoundaryProps, State> {
7+
constructor(props: ErrorBoundaryProps) {
8+
super(props)
9+
10+
// Define a state variable to track whether is an error or not
11+
this.state = { hasError: false }
12+
}
13+
static getDerivedStateFromError(error: any) {
14+
// Update state so the next render will show the fallback UI
15+
16+
return { hasError: true }
17+
}
18+
componentDidCatch(error: any, errorInfo: any) {
19+
// You can use your own error logging service here
20+
console.log({ error, errorInfo })
21+
}
22+
render() {
23+
// Check if the error is thrown
24+
if (this.state.hasError) {
25+
// You can render any custom fallback UI
26+
// return (
27+
// <div>
28+
// <h2>Oops, there is an error!</h2>
29+
// <button type="button" onClick={() => this.setState({ hasError: false })}>
30+
// Try again?
31+
// </button>
32+
// </div>
33+
// )
34+
}
35+
36+
// Return children components in case of no error
37+
return this.props.children
38+
}
39+
}
40+
41+
export default ErrorBoundary
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './ErrorBoundary.jsx'

apps/book-web/src/pages/_app.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import '@packages/nextra-editor/style.css'
22
import '../styles/global.css'
3+
import { ErrorBoundary } from 'next/dist/client/components/error-boundary'
34

45
import type { AppProps } from 'next/app'
56
import CoreProvider from '@/provider/CoreProvider'
@@ -11,11 +12,13 @@ type Props = {
1112

1213
const App = ({ Component, pageProps }: Props) => {
1314
return (
14-
<CoreProvider>
15-
<HydrationBoundary state={pageProps.dehydratedProps}>
16-
<Component {...pageProps} />
17-
</HydrationBoundary>
18-
</CoreProvider>
15+
<ErrorBoundary errorComponent={undefined}>
16+
<CoreProvider>
17+
<HydrationBoundary state={pageProps.dehydratedProps}>
18+
<Component {...pageProps} />
19+
</HydrationBoundary>
20+
</CoreProvider>
21+
</ErrorBoundary>
1922
)
2023
}
2124

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { UploadImageArgs } from '@/api/routes/files'
2+
import { EditorSelection } from '@codemirror/state'
3+
import { EditorView } from 'codemirror'
4+
5+
export const handleDrop = (
6+
event: DragEvent,
7+
view: EditorView,
8+
upload: (args: UploadImageArgs) => Promise<string>,
9+
): void => {
10+
if (!event.dataTransfer) return
11+
if (event.dataTransfer.files.length === 0) return
12+
13+
event.preventDefault()
14+
15+
const files = event.dataTransfer?.files
16+
17+
const itemArray = []
18+
for (let i = 0; i < files.length; i++) {
19+
itemArray.push(files[i])
20+
}
21+
22+
const tempUrls: string[] = []
23+
const images = itemArray
24+
.filter((item) => item.type.includes('image'))
25+
.map((item) => {
26+
const tempUrl = URL.createObjectURL(item)
27+
const main = view.state.selection.main
28+
let insert = `![]()`
29+
30+
if (tempUrl) {
31+
insert = `![업로드중...](${tempUrl})\n`
32+
}
33+
34+
tempUrls.push(insert)
35+
view.dispatch({
36+
changes: {
37+
from: main.from,
38+
to: main.from,
39+
insert,
40+
},
41+
})
42+
return item
43+
})
44+
45+
const promises = images.map((item) => upload({ file: item, info: { type: 'book', refId: '' } }))
46+
47+
Promise.allSettled(promises).then((result) => {
48+
let lastCursorPosition = view.state.selection.main.from
49+
result.map((image, index) => {
50+
if (image.status !== 'fulfilled') return
51+
const tempUrl = tempUrls[index]
52+
const imageUrl = `![](${image.value})\n`
53+
54+
// replace temp url to real url
55+
const currentContent = view.state.doc.toString()
56+
const tempUrlIndex = currentContent.indexOf('![업로드중...]', lastCursorPosition)
57+
58+
if (tempUrlIndex !== -1) {
59+
view.dispatch({
60+
changes: {
61+
from: tempUrlIndex,
62+
to: tempUrlIndex + tempUrl.length,
63+
insert: imageUrl,
64+
},
65+
selection: EditorSelection.cursor(tempUrlIndex + imageUrl.length),
66+
})
67+
68+
lastCursorPosition = tempUrlIndex + imageUrl.length
69+
}
70+
})
71+
72+
// 모든 이미지 처리 후 커서를 마지막 이미지 다음으로 이동
73+
view.dispatch({
74+
selection: EditorSelection.cursor(lastCursorPosition),
75+
})
76+
})
77+
}

packages/nextra-editor/src/components/markdown-editor/events/paste.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ export const handlePaste = (
1010
const clipboardData = event.clipboardData
1111
if (!clipboardData) return
1212
const text = clipboardData.getData('Text')
13-
1413
if (text) return
15-
14+
1615
event.preventDefault()
1716

1817
const itemArray = []

packages/nextra-editor/src/components/markdown-editor/hooks/useCodemirror.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
1010
import { languages } from '@codemirror/language-data'
1111
import { handlePaste } from '../events/paste'
1212
import { useUpload } from '@/hooks/use-upload'
13+
import { handleDrop } from '../events/drop'
1314

1415
type Config = {
1516
autoFocus?: boolean
@@ -41,9 +42,7 @@ export const useCodemirror = (container: RefObject<HTMLElement>, config: Config
4142

4243
const eventHandlers = EditorView.domEventHandlers({
4344
paste: (event, view) => handlePaste(event, view, upload),
44-
drop: (event, view) => {
45-
console.log('envet', event.dataTransfer?.files)
46-
},
45+
drop: (event, view) => handleDrop(event, view, upload),
4746
})
4847

4948
const defaultThemeOption = EditorView.theme({

packages/nextra-editor/src/components/markdown-editor/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import cn from 'clsx'
21
import { useRef } from 'react'
32
import { useCodemirror } from './hooks/useCodemirror'
43
import Toolbar from './toolbar'

0 commit comments

Comments
 (0)