- Download all notes as Markdown files in a zip.
+ Download all notes as files in a zip.
+
Export TakeNote data as JSON.
{
+ // Remove whitespace from both ends
+ // Get the first n characters
+ // Remove # from the title in the case of using markdown headers in your title
+ const noteText = text.trim().match(/[^#]{1,45}/)
+
+ // Get the first line of text after any newlines
+ // In the future, this should break on a full word
+ return noteText ? noteText[0].trim().split(/\r?\n/)[0] : LabelText.NEW_NOTE
+}
+
+export const noteWithFrontmatter = (note: NoteItem, category?: CategoryItem): string =>
+ `---
+title: ${getNoteTitle(note.text)}
+created: ${note.created}
+lastUpdated: ${note.lastUpdated}
+category: ${category?.name ?? ''}
+---
+
+${note.text}`
+
+function* downloadAsPDF({ payload }: DownloadPDFAction) {
+ try {
+ const { notes } = payload
+ const headers = {
+ 'Content-Type': 'application/pdf',
+ }
+ if (notes.length === 1) {
+ yield axios({
+ method: 'POST',
+ url: '/api/note/download',
+ data: payload,
+ responseType: 'blob',
+ }).then((res) => {
+ const file = new Blob([res.data], { type: 'application/pdf' })
+ const link = document.createElement('a')
+ const fileURL = URL.createObjectURL(file)
+ link.href = fileURL
+ link.setAttribute('download', `${getNoteTitle(notes[0].text)}.pdf`)
+ document.body.appendChild(link)
+ if (document.createEvent) {
+ const event = document.createEvent('MouseEvents')
+ event.initEvent('click', true, true)
+ link.dispatchEvent(event)
+ } else {
+ link.click()
+ }
+ })
+ } else {
+ yield axios({
+ method: 'POST',
+ url: '/api/note/downloadAll',
+ data: payload,
+ responseType: 'blob',
+ }).then((res) => {
+ const file = new Blob([res.data], { type: 'application/zip' })
+ const link = document.createElement('a')
+ const fileURL = URL.createObjectURL(file)
+ link.href = fileURL
+ link.setAttribute('download', `notesPDF.zip`)
+ document.body.appendChild(link)
+ if (document.createEvent) {
+ const event = document.createEvent('MouseEvents')
+ event.initEvent('click', true, true)
+ link.dispatchEvent(event)
+ } else {
+ link.click()
+ }
+ })
+ }
+ } catch (error) {
+ yield put(loadCategoriesError(error.message))
+ }
+}
+
// If any of these functions are dispatched, invoke the appropriate saga
function* rootSaga() {
yield all([
takeLatest(login.type, loginUser),
takeLatest(logout.type, logoutUser),
takeLatest(loadNotes.type, fetchNotes),
+ takeLatest(downloadPDFNotes.type, downloadAsPDF),
takeLatest(loadCategories.type, fetchCategories),
takeLatest(loadSettings.type, fetchSettings),
takeLatest(sync.type, syncData),
diff --git a/src/client/slices/note.ts b/src/client/slices/note.ts
index 808aae1c..4a5d77cc 100644
--- a/src/client/slices/note.ts
+++ b/src/client/slices/note.ts
@@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { v4 as uuid } from 'uuid'
import { Folder, NotesSortKey } from '@/utils/enums'
-import { NoteItem, NoteState } from '@/types'
+import { NoteItem, NoteState, CategoryItem } from '@/types'
import { isDraftNote } from '@/utils/helpers'
import { getNotesSorter } from '@/utils/notesSortStrategies'
@@ -313,6 +313,12 @@ const noteSlice = createSlice({
]
state.loading = false
},
+ downloadPDFNotes: (
+ state,
+ { payload }: PayloadAction<{ notes: NoteItem[]; categories: CategoryItem[] }>
+ ) => {
+ state.loading = false
+ },
},
})
@@ -338,6 +344,7 @@ export const {
loadNotesError,
loadNotesSuccess,
importNotes,
+ downloadPDFNotes,
} = noteSlice.actions
export default noteSlice.reducer
diff --git a/src/client/types/index.ts b/src/client/types/index.ts
index ee3434bf..4ece14ad 100644
--- a/src/client/types/index.ts
+++ b/src/client/types/index.ts
@@ -102,6 +102,11 @@ export interface SyncAction {
payload: SyncPayload
}
+export interface DownloadPDFAction {
+ type: typeof sync.type
+ payload: SyncPayload
+}
+
//==============================================================================
// Events
//==============================================================================
diff --git a/src/resources/LabelText.ts b/src/resources/LabelText.ts
index 43d02310..8688b5c3 100644
--- a/src/resources/LabelText.ts
+++ b/src/resources/LabelText.ts
@@ -22,7 +22,8 @@ export enum LabelText {
WELCOME_TO_TAKENOTE = 'Welcome to Takenote!',
RENAME = 'Rename category',
ADD_CONTENT_NOTE = 'Please add content to this new note to access the menu options.',
- DOWNLOAD_ALL_NOTES = 'Download all notes',
+ DOWNLOAD_ALL_NOTES = 'Download all notes as Markdown',
+ DOWNLOAD_ALL_NOTES_PDF = 'Download all notes as PDF',
BACKUP_ALL_NOTES = 'Export backup',
IMPORT_BACKUP = 'Import backup',
TOGGLE_FAVORITE = 'Toggle favorite',
diff --git a/src/server/handlers/note.ts b/src/server/handlers/note.ts
new file mode 100644
index 00000000..591cd870
--- /dev/null
+++ b/src/server/handlers/note.ts
@@ -0,0 +1,70 @@
+import { Request, Response } from 'express'
+import puppeteer from 'puppeteer'
+import marked from 'marked'
+import JSZip from 'jszip'
+
+import { NoteItem, CategoryItem } from '@/types'
+
+export const getNoteTitle = (text: string): string => {
+ // Remove whitespace from both ends
+ // Get the first n characters
+ // Remove # from the title in the case of using markdown headers in your title
+ const noteText = text.trim().match(/[^#]{1,45}/)
+
+ // Get the first line of text after any newlines
+ // In the future, this should break on a full word
+ return noteText ? noteText[0].trim().split(/\r?\n/)[0] : 'New note'
+}
+
+export const noteWithFrontmatter = (note: NoteItem, category?: CategoryItem): string =>
+ `---
+title: ${getNoteTitle(note.text)}
+created: ${note.created}
+lastUpdated: ${note.lastUpdated}
+category: ${category?.name ?? ''}
+---
+
+${note.text}`
+
+export default {
+ download: async (request: Request, response: Response) => {
+ const {
+ body: { notes },
+ } = request
+ const element = marked(notes[0].text)
+ const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] })
+ const page = await browser.newPage()
+ await page.setContent(element)
+ await page.pdf({ format: 'a4' }).then((pdf) => {
+ response.setHeader('Content-Disposition', `attachment; filename=123.pdf`)
+ response.send(pdf)
+ })
+ },
+ downloadAll: async (request: Request, response: Response) => {
+ const {
+ body: { notes },
+ } = request
+ const pdfFiles: any = []
+ const fileNames: string[] = []
+ const zip = new JSZip()
+ await Promise.all(
+ notes.map(async (note: any) => {
+ const element = marked(note.text)
+ const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] })
+ const page = await browser.newPage()
+ await page.setContent(element)
+ const pdf = await page.pdf({ format: 'a4' })
+ pdfFiles.push(pdf)
+ fileNames.push(`${getNoteTitle(note.text)} (${note.id.substring(0, 6)}).pdf`)
+
+ return pdf
+ })
+ )
+
+ pdfFiles.map((files: any, index: number) => zip.file(fileNames[index], files))
+ zip.generateAsync({ type: 'nodebuffer' }).then((content) => {
+ response.setHeader('Content-Disposition', `attachment; filename=notes.zip`)
+ response.send(content)
+ })
+ },
+}
diff --git a/src/server/router/index.ts b/src/server/router/index.ts
index 31ddb2e2..e8381b22 100644
--- a/src/server/router/index.ts
+++ b/src/server/router/index.ts
@@ -2,10 +2,12 @@ import express from 'express'
import authRoutes from './auth'
import syncRoutes from './sync'
+import noteRoutes from './note'
const router = express.Router()
router.use('/auth', authRoutes)
router.use('/sync', syncRoutes)
+router.use('/note', noteRoutes)
export default router
diff --git a/src/server/router/note.ts b/src/server/router/note.ts
new file mode 100644
index 00000000..2aa1c3e3
--- /dev/null
+++ b/src/server/router/note.ts
@@ -0,0 +1,15 @@
+import express from 'express'
+
+import noteHandler from '../handlers/note'
+import checkAuth from '../middleware/checkAuth'
+import getUser from '../middleware/getUser'
+
+const router = express.Router()
+
+router.post('/download', noteHandler.download)
+router.post('/downloadAll', noteHandler.downloadAll)
+router.post('/health', (req, res) => {
+ res.send(200)
+})
+
+export default router