diff --git a/components/button-progress.tsx b/components/button-progress.tsx
new file mode 100644
index 0000000..d605d4b
--- /dev/null
+++ b/components/button-progress.tsx
@@ -0,0 +1,29 @@
+import { Button, ButtonProps, CircularProgress } from '@material-ui/core'
+import classNames from 'classnames'
+import { FC } from 'react'
+
+export const ButtonProgress: FC<
+ ButtonProps & {
+ loading?: boolean
+ progress?: number
+ }
+> = ({ children, loading, progress, ...props }) => {
+ return (
+
+ )
+}
diff --git a/components/editor/note-edit.tsx b/components/editor/note-edit.tsx
index 71d425b..2c98e70 100644
--- a/components/editor/note-edit.tsx
+++ b/components/editor/note-edit.tsx
@@ -7,6 +7,7 @@ import { useDebouncedCallback } from 'use-debounce'
import EditTitle from './edit-title'
import Editor from './editor'
import { NoteModel } from 'libs/shared/note'
+import { ROOT_ID } from 'libs/shared/tree'
const NoteEdit = () => {
const { updateNote, createNote, note } = NoteState.useContainer()
@@ -17,7 +18,7 @@ const NoteEdit = () => {
const isNew = has(router.query, 'new')
if (isNew) {
- data.pid = (router.query.pid as string) || 'root'
+ data.pid = (router.query.pid as string) || ROOT_ID
const item = await createNote({ ...note, ...data })
const noteUrl = `/${item?.id}`
diff --git a/components/settings/daily-notes-field.tsx b/components/settings/daily-notes.tsx
similarity index 93%
rename from components/settings/daily-notes-field.tsx
rename to components/settings/daily-notes.tsx
index e50e4de..c487acf 100644
--- a/components/settings/daily-notes-field.tsx
+++ b/components/settings/daily-notes.tsx
@@ -5,10 +5,10 @@ import NoteTreeState from 'libs/web/state/tree'
import { filter } from 'lodash'
import UIState from 'libs/web/state/ui'
import { TreeItemModel } from 'libs/shared/tree'
-import { defaultFieldConfig } from './settings-form'
+import { defaultFieldConfig } from './settings-container'
import useI18n from 'libs/web/hooks/use-i18n'
-export const DailyNotesField: FC = () => {
+export const DailyNotes: FC = () => {
const { t } = useI18n()
const { tree } = NoteTreeState.useContainer()
const {
diff --git a/components/settings/export-button.tsx b/components/settings/export-button.tsx
new file mode 100644
index 0000000..78403d8
--- /dev/null
+++ b/components/settings/export-button.tsx
@@ -0,0 +1,16 @@
+import { FC } from 'react'
+import useI18n from 'libs/web/hooks/use-i18n'
+import { ButtonProps } from './type'
+import { ROOT_ID } from 'libs/shared/tree'
+import Link from 'next/link'
+import { ButtonProgress } from 'components/button-progress'
+
+export const ExportButton: FC = ({ parentId = ROOT_ID }) => {
+ const { t } = useI18n()
+
+ return (
+
+ {t('Export')}
+
+ )
+}
diff --git a/components/settings/import-button.tsx b/components/settings/import-button.tsx
new file mode 100644
index 0000000..50e7ee5
--- /dev/null
+++ b/components/settings/import-button.tsx
@@ -0,0 +1,86 @@
+import { ChangeEvent, FC, useCallback, useEffect } from 'react'
+import useI18n from 'libs/web/hooks/use-i18n'
+import { ButtonProps } from './type'
+import useFetcher from 'libs/web/api/fetcher'
+import { useToast } from 'libs/web/hooks/use-toast'
+import { ButtonProgress } from 'components/button-progress'
+import { IMPORT_FILE_LIMIT_SIZE } from 'libs/shared/const'
+import { useRouter } from 'next/router'
+import { ROOT_ID } from 'libs/shared/tree'
+
+export const ImportButton: FC = ({ parentId = ROOT_ID }) => {
+ const { t } = useI18n()
+ const { request, loading, error } = useFetcher()
+ const toast = useToast()
+ const router = useRouter()
+
+ const onSelectFile = useCallback(
+ async (event: ChangeEvent) => {
+ const data = new FormData()
+
+ if (!event.target.files?.length) {
+ return toast(t('Please select zip file'), 'error')
+ }
+
+ const file = event.target.files[0]
+
+ if (file.size > IMPORT_FILE_LIMIT_SIZE) {
+ return toast(
+ t('File size must be less than {{n}}mb', {
+ n: IMPORT_FILE_LIMIT_SIZE / 1024 / 1024,
+ }),
+ 'error'
+ )
+ }
+
+ data.append('file', file)
+
+ const result = await request<
+ FormData,
+ { total: number; imported: number }
+ >(
+ {
+ method: 'POST',
+ url: '/api/import?pid=' + parentId,
+ },
+ data
+ )
+
+ event.target.value = ''
+ if (!result) return
+ if (!result?.imported) {
+ return toast(t('Not found markdown file'), 'warning')
+ }
+ toast(
+ t('Successfully imported {{n}} markdown files', {
+ n: result?.imported,
+ }),
+ 'success'
+ )
+ /**
+ * @todo fetch tree without reload page
+ */
+ router.reload()
+ },
+ [parentId, request, router, t, toast]
+ )
+
+ useEffect(() => {
+ if (error) {
+ toast(error, 'error')
+ }
+ }, [error, toast])
+
+ return (
+
+ )
+}
diff --git a/components/settings/import-or-export.tsx b/components/settings/import-or-export.tsx
new file mode 100644
index 0000000..de8949a
--- /dev/null
+++ b/components/settings/import-or-export.tsx
@@ -0,0 +1,51 @@
+import { FC, useCallback, useMemo, useState } from 'react'
+import { TextField } from '@material-ui/core'
+import { defaultFieldConfig } from './settings-container'
+import useI18n from 'libs/web/hooks/use-i18n'
+import { ROOT_ID, TreeItemModel } from 'libs/shared/tree'
+import NoteTreeState from 'libs/web/state/tree'
+import { filter } from 'lodash'
+import { Autocomplete } from '@material-ui/lab'
+import { ExportButton } from './export-button'
+import { ImportButton } from './import-button'
+
+export const ImportOrExport: FC = () => {
+ const { t } = useI18n()
+ const { tree } = NoteTreeState.useContainer()
+ const [selected, setSelected] = useState(tree.items[ROOT_ID])
+ const items = useMemo(
+ () => filter(tree.items, (item) => !item.data?.deleted),
+ [tree]
+ )
+
+ const handleChange = useCallback((_event, item: TreeItemModel | null) => {
+ if (item) {
+ setSelected(item)
+ }
+ }, [])
+
+ return (
+
+
option.data?.title || t('Root Page')}
+ value={selected}
+ onChange={handleChange}
+ renderInput={(params) => (
+
+ )}
+ >
+
+
+
+
+
+ )
+}
diff --git a/components/settings/language-field.tsx b/components/settings/language.tsx
similarity index 90%
rename from components/settings/language-field.tsx
rename to components/settings/language.tsx
index 4e1e193..47e6a7e 100644
--- a/components/settings/language-field.tsx
+++ b/components/settings/language.tsx
@@ -1,13 +1,13 @@
import { FC, useCallback, ChangeEvent } from 'react'
import { MenuItem, TextField } from '@material-ui/core'
import UIState from 'libs/web/state/ui'
-import { defaultFieldConfig } from './settings-form'
+import { defaultFieldConfig } from './settings-container'
import router from 'next/router'
import useI18n from 'libs/web/hooks/use-i18n'
import { configLocale, Locale } from 'locales'
import { map } from 'lodash'
-export const LanguageField: FC = () => {
+export const Language: FC = () => {
const { t } = useI18n()
const {
settings: { settings, updateSettings },
diff --git a/components/settings/setting-footer.tsx b/components/settings/setting-footer.tsx
index b400b9c..c48f25f 100644
--- a/components/settings/setting-footer.tsx
+++ b/components/settings/setting-footer.tsx
@@ -2,7 +2,7 @@ import pkg from 'package.json'
export const SettingFooter = () => {
return (
-