Skip to content

Commit

Permalink
feat: Add NoteInfo and NoteContent types
Browse files Browse the repository at this point in the history
Add new types NoteInfo and NoteContent to the shared/models.ts file. These types represent the structure of a note's information and content.
  • Loading branch information
PenguScript committed Oct 27, 2024
1 parent 6383dfa commit 23c4681
Show file tree
Hide file tree
Showing 30 changed files with 2,932 additions and 23 deletions.
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0"
"@electron-toolkit/utils": "^3.0.0",
"@mdxeditor/editor": "^3.14.0",
"fs-extra": "^11.2.0",
"jotai": "^2.10.1"
},
"devDependencies": {
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@tailwindcss/typography": "^0.5.15",
"@types/node": "^20.14.8",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
Expand All @@ -39,6 +43,7 @@
"electron-vite": "^2.3.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.3",
"lodash": "^4.17.21",
"postcss": "^8.4.47",
"prettier": "^3.3.2",
"react": "^18.3.1",
Expand Down
54 changes: 54 additions & 0 deletions resources/welcomeNote.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
## Welcome to NoteMark 👋🏻

NoteMark is a simple **note-taking app** that uses **Markdown** syntax to format your notes.

You can create your first note by clicking on the top-left icon on the sidebar, or delete one by clicking on top right icon.

Following there's a quick overview of the currently supported Markdown syntax.

## Text formatting

This is a **bold** text.
This is an _italic_ text.

## Headings

Here are all the heading formats currently supported by **_NoteMark_**:

# Heading 1

## Heading 2

### Heading 3

#### Heading 4

### Bulleted list

For example, you can add a list of bullet points:

- Bullet point 1
- Bullet point 2
- Bullet point 3

### Numbered list

Here we have a numbered list:

1. Numbered list item 1
2. Numbered list item 2
3. Numbered list item 3

### Blockquote

> This is a blockquote. You can use it to emphasize some text or to cite someone.
### Code blocks

Only `inline code` is currently supported!

Code block snippets using the following syntax _\`\`\`js\`\`\`_ are **_not supported_** yet!

### Links

Links are **_not supported_** yet!
9 changes: 7 additions & 2 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
import { color } from './../../node_modules/tailwindcss/src/util/dataTypes';
import { createNote, getNotes, readNote, writeNote, deleteNote } from './lib';
import { GetNotes, ReadNote, WriteNote, CreateNote, DeleteNote } from '@shared/types';

function createWindow(): void {
// Create the browser window.
Expand Down Expand Up @@ -58,7 +59,11 @@ app.whenReady().then(() => {
})

// IPC test
ipcMain.on('ping', () => console.log('pong'))
ipcMain.handle('getNotes', (_, ...args: Parameters<GetNotes>) => getNotes(...args))
ipcMain.handle('readNote', (_, ...args: Parameters<ReadNote>) => readNote(...args))
ipcMain.handle('writeNote', (_, ...args: Parameters<WriteNote>) => writeNote(...args))
ipcMain.handle('createNote', (_, ...args: Parameters<CreateNote>) => createNote(...args))
ipcMain.handle('deleteNote', (_, ...args: Parameters<DeleteNote>) => deleteNote(...args))

createWindow()

Expand Down
119 changes: 119 additions & 0 deletions src/main/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { appDirectoryName, fileEncoding, welcomeNoteFilename } from "@shared/constants"
import { NoteInfo } from "@shared/models"
import { CreateNote, DeleteNote, GetNotes, ReadNote, WriteNote } from "@shared/types"
import { dialog } from "electron"
import { ensureDir, readdir, stat, readFile, writeFile, remove } from "fs-extra"
import { homedir } from "os"
import path from "path"
import welcomeNoteFile from "../../../resources/welcomeNote.md?asset"

export const getRootDir = () => {
return `${homedir()}\\${appDirectoryName}`
}

export const getNotes: GetNotes = async () => {
const rootDir = getRootDir()

await ensureDir(rootDir)

const notesFileNames = await readdir(rootDir, {
encoding: fileEncoding,
withFileTypes: false
})

const notes = notesFileNames.filter((fileName) => fileName.endsWith(".md"))

if (notes.length === 0) {
console.info("No notes found, creating a welcome note")

const content = await readFile(welcomeNoteFile, { encoding: fileEncoding })

await writeFile(`${rootDir}/${welcomeNoteFilename}`, content, { encoding: fileEncoding })

notes.push(welcomeNoteFilename)
}

return Promise.all(notes.map(getNoteInfoFromFilename))
}

export const getNoteInfoFromFilename = async (fileName: string): Promise<NoteInfo> => {
const fileStats = await stat(`${getRootDir()}/${fileName}`)

return {
title: fileName.replace(/\.md$/, ""),
lastEdited: fileStats.mtimeMs
}
}

export const readNote: ReadNote = async (filename) => {
const rootDir = getRootDir()

return readFile(`${rootDir}/${filename}.md`, { encoding: fileEncoding })
}

export const writeNote: WriteNote = async (filename, content) => {
const rootDir = getRootDir()

console.info(`Writing note ${filename}`)
return writeFile(`${rootDir}/${filename}.md`, content, { encoding: fileEncoding })
}

export const createNote: CreateNote = async () => {
const rootDir = getRootDir()

await ensureDir(rootDir)

const {filePath, canceled} = await dialog.showSaveDialog({
title: "New note",
defaultPath: `${rootDir}/Untitled.md`,
buttonLabel: "Create",
properties: ['showOverwriteConfirmation'],
showsTagField: false,
filters: [{ name: "Markdown", extensions: ["md"] }]
})

if (canceled || !filePath) {
console.info("Note creation canceled")
return false
}

const {name: filename, dir: parentDir} = path.parse(filePath)

if (parentDir !== rootDir) {
await dialog.showMessageBox({
type: 'error',
title: 'Creation failed',
message: `All notes must be saved under ${rootDir}.
Avoid using other directories!`
})

return false
}

console.info(`Creating note ${filename}`)
await writeFile(filePath, "")

return filename
}

export const deleteNote: DeleteNote = async (filename) => {
const rootDir = getRootDir()

const { response } = await dialog.showMessageBox({
type: 'warning',
title: 'Delete Note',
message: `Are you sure you want to delete ${filename}?`,
buttons: ['Delete', 'Cancel'],
defaultId: 1,
cancelId: 1
})

if (response === 1) {
console.info(`Note deletion canceled`)
return false
}

console.info(`Deleting note ${filename}`)
await remove(`${rootDir}/${filename}.md`)
return true
}
10 changes: 9 additions & 1 deletion src/preload/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import { CreateNote, DeleteNote, GetNotes, ReadNote, WriteNote } from '@shared/types'

declare global {
interface Window {
context: {}
context: {
locale: string
getNotes: GetNotes
readNote: ReadNote
writeNote: WriteNote
createNote: CreateNote
deleteNote: DeleteNote
}
}
}
11 changes: 8 additions & 3 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
import { contextBridge, ipcRenderer } from 'electron'
import { CreateNote, DeleteNote, GetNotes, ReadNote, WriteNote } from '@shared/types'

if(!process.contextIsolated) {
throw new Error('contextIsolation must be enabled in the BrowserWindow')
}
try {
contextBridge.exposeInMainWorld('context', {
//TODO
locale: navigator.language,
getNotes: (...args: Parameters<GetNotes>) => ipcRenderer.invoke('getNotes', ...args),
readNote: (...args: Parameters<ReadNote>) => ipcRenderer.invoke('readNote', ...args),
writeNote: (...args: Parameters<WriteNote>) => ipcRenderer.invoke('writeNote', ...args),
createNote: (...args: Parameters<CreateNote>) => ipcRenderer.invoke('createNote', ...args),
deleteNote: (...args: Parameters<DeleteNote>) => ipcRenderer.invoke('deleteNote', ...args),
})
} catch (error) {
console.error(error)
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
content="default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
</head>

Expand Down
22 changes: 19 additions & 3 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import { Content, RootLayout, Sidebar, ActionButtonsRow, NotePreviewList, Editor, NoteTitle } from "@/components"
import { useRef } from "react"

function App(): JSX.Element {
const contentContainerRef = useRef<HTMLDivElement>(null)

const resetScroll = () => {
contentContainerRef.current?.scrollTo(0, 0)
}

return (
<div className="flex h-full items-center justify-center">
<span className="text-4xl text-blue-800">Hello World!</span>
</div>
<RootLayout>
<Sidebar className="p-2">
<ActionButtonsRow className="flex justify-between mt-1" />
<NotePreviewList className="mt-3 space-y-1" onSelect={resetScroll} />
</Sidebar>
<Content ref={contentContainerRef} className="border-l bg-neutral-900/50 border-l-white/20">
<NoteTitle className="pt-2" />
<Editor />
</Content>
</RootLayout>
)
}

Expand Down
12 changes: 12 additions & 0 deletions src/renderer/src/assets/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,16 @@

@apply overflow-hidden;
}
}

::-webkit-scrollbar {
@apply w-2;
}

::-webkit-scrollbar-thumb {
@apply bg-neutral-700 rounded-md;
}

::-webkit-scrollbar-track {
@apply bg-transparent;
}
11 changes: 11 additions & 0 deletions src/renderer/src/components/ActionButtonsRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DeleteNoteButton, NewNoteButton } from "@/components";
import { ComponentProps } from 'react';

export const ActionButtonsRow = ({...props }: ComponentProps<'div'>) => {
return (
<div {...props}>
<NewNoteButton />
<DeleteNoteButton />
</div>
)
}
18 changes: 18 additions & 0 deletions src/renderer/src/components/Button/ActionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ComponentProps } from 'react'
import { twMerge } from 'tailwind-merge'

export type ActionButtonProps = ComponentProps<'button'>

export const ActionButton = ({ children, className, ...props }: ActionButtonProps) => {
return (
<button
className={twMerge(
'px-2 py-1 rounded-md border border-neutral-400/50 hover:bg-neutral-600/50 transition-colors duration-100',
className
)}
{...props}
>
{children}
</button>
)
}
18 changes: 18 additions & 0 deletions src/renderer/src/components/Button/DeleteNoteButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { deleteNoteAtom } from "@renderer/store";
import { ActionButton, ActionButtonProps } from "./ActionButton";
import { IoMdTrash } from "react-icons/io";
import { useSetAtom } from "jotai";

export const DeleteNoteButton = ({...props}: ActionButtonProps) => {
const deleteNote = useSetAtom(deleteNoteAtom)

const handleDelete = async () => {
await deleteNote()
}

return (
<ActionButton onClick={handleDelete} {...props}>
<IoMdTrash className="w-4 h-4 text-neutral-300"/>
</ActionButton>
);
}
18 changes: 18 additions & 0 deletions src/renderer/src/components/Button/NewNoteButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ActionButton, ActionButtonProps } from "@/components";
import { createEmptyNoteAtom } from "@renderer/store";
import { useSetAtom } from "jotai";
import { MdOutlineNoteAdd } from "react-icons/md";

export const NewNoteButton = ({...props}: ActionButtonProps) => {
const createEmptyNote = useSetAtom(createEmptyNoteAtom)

const handleCreation = async () => {
await createEmptyNote()
}

return (
<ActionButton onClick={handleCreation} {...props}>
<MdOutlineNoteAdd className="w-4 h-4 text-neutral-300"/>
</ActionButton>
);
}
3 changes: 3 additions & 0 deletions src/renderer/src/components/Button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './ActionButton'
export * from './NewNoteButton'
export * from './DeleteNoteButton'
Loading

0 comments on commit 23c4681

Please sign in to comment.