diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..6309eac
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,10 @@
+{
+ "semi": false,
+ "singleQuote": true,
+ "tabWidth": 2,
+ "printWidth": 100,
+ "trailingComma": "es5",
+ "bracketSpacing": true,
+ "arrowParens": "avoid",
+ "endOfLine": "lf"
+}
\ No newline at end of file
diff --git a/eslint.config.js b/eslint.config.js
index d94e7de..84f9297 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -12,12 +12,92 @@ export default tseslint.config([
extends: [
js.configs.recommended,
tseslint.configs.recommended,
+ tseslint.configs.strict,
+ tseslint.configs.stylistic,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
+ parserOptions: {
+ projectService: true,
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ rules: {
+ '@typescript-eslint/no-explicit-any': 'error',
+ '@typescript-eslint/explicit-function-return-type': ['warn', {
+ allowExpressions: true,
+ allowTypedFunctionExpressions: true,
+ allowHigherOrderFunctions: true,
+ allowDirectConstAssertionInArrowFunctions: true,
+ allowConciseArrowFunctionExpressionsStartingWithVoid: true,
+ }],
+ '@typescript-eslint/no-non-null-assertion': 'warn',
+ '@typescript-eslint/strict-boolean-expressions': ['warn', {
+ allowNullableObject: true,
+ allowNullableBoolean: true,
+ allowNullableString: true,
+ allowNullableNumber: false,
+ allowAny: false,
+ }],
+ '@typescript-eslint/no-unnecessary-condition': 'error',
+ '@typescript-eslint/no-unsafe-assignment': 'error',
+ '@typescript-eslint/no-unsafe-member-access': 'error',
+ '@typescript-eslint/no-unsafe-call': 'error',
+ '@typescript-eslint/no-unsafe-return': 'error',
+ '@typescript-eslint/no-floating-promises': 'error',
+ '@typescript-eslint/no-misused-promises': 'error',
+ '@typescript-eslint/await-thenable': 'error',
+ '@typescript-eslint/require-await': 'error',
+ '@typescript-eslint/no-unnecessary-type-assertion': 'error',
+ '@typescript-eslint/prefer-readonly': 'error',
+ '@typescript-eslint/array-type': ['error', { default: 'array-simple' }],
+ '@typescript-eslint/consistent-type-imports': ['error', {
+ prefer: 'type-imports',
+ disallowTypeAnnotations: true,
+ fixStyle: 'separate-type-imports',
+ }],
+ '@typescript-eslint/naming-convention': [
+ 'error',
+ {
+ selector: 'variable',
+ format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
+ },
+ {
+ selector: 'function',
+ format: ['camelCase', 'PascalCase'],
+ },
+ {
+ selector: 'typeLike',
+ format: ['PascalCase'],
+ },
+ {
+ selector: 'enum',
+ format: ['PascalCase'],
+ },
+ {
+ selector: 'enumMember',
+ format: ['UPPER_CASE'],
+ },
+ ],
+ 'eqeqeq': ['error', 'always'],
+ 'no-console': 'warn',
+ 'no-debugger': 'error',
+ 'no-alert': 'error',
+ 'prefer-const': 'error',
+ 'no-var': 'error',
+ 'object-shorthand': 'error',
+ 'prefer-template': 'error',
+ 'prefer-destructuring': ['error', {
+ array: true,
+ object: true,
+ }],
+ 'no-nested-ternary': 'error',
+ 'no-unneeded-ternary': 'error',
+ 'curly': ['error', 'all'],
+ 'brace-style': ['error', '1tbs'],
},
},
])
diff --git a/package.json b/package.json
index ffc8a72..8028a98 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,8 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "typecheck": "tsc -b --noEmit",
"preview": "vite preview"
},
"dependencies": {
diff --git a/src/components/NavigationConsole.tsx b/src/components/NavigationConsole.tsx
index 48ebc7a..d3f09b7 100644
--- a/src/components/NavigationConsole.tsx
+++ b/src/components/NavigationConsole.tsx
@@ -52,7 +52,9 @@ export function NavigationConsole({ isOpen, onClose }: NavigationConsoleProps) {
}, [input, existingSpaces])
const handleShare = async () => {
- if (!space) return
+ if (!space) {
+return
+}
const shareLink = generateShareLink(space)
const success = await copyToClipboard(shareLink)
@@ -109,7 +111,9 @@ export function NavigationConsole({ isOpen, onClose }: NavigationConsoleProps) {
onClose()
}
- if (!isOpen) return null
+ if (!isOpen) {
+return null
+}
return (
<>
diff --git a/src/components/Note.tsx b/src/components/Note.tsx
index 27dc9d5..449cd9e 100644
--- a/src/components/Note.tsx
+++ b/src/components/Note.tsx
@@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from 'react'
import { updateNote, deleteNote, editingNoteId, navigateToSpace, selectedNoteIds, selectNote, moveSelectedNotesLocal, saveSelectedNotesToDB, existingSpaceIds } from '../store'
import { useStore } from '@nanostores/react'
-type NoteType = {
+interface NoteType {
id: string
x: number
y: number
@@ -29,10 +29,18 @@ export function Note({ note }: NoteProps) {
// Check if note content is a single word that matches an existing space
const isSpaceLink = () => {
- if (!note.content) return false
+ if (!note.content) {
+return false
+}
const words = note.content.trim().split(/\s+/)
- if (words.length !== 1) return false
- const spaceId = words[0].toLowerCase().replace(/[^a-z0-9]/g, '-')
+ if (words.length !== 1) {
+return false
+}
+ const firstWord = words[0]
+ if (!firstWord) {
+return false
+}
+ const spaceId = firstWord.toLowerCase().replace(/[^a-z0-9]/g, '-')
return $existingSpaceIds.includes(spaceId)
}
@@ -72,7 +80,9 @@ export function Note({ note }: NoteProps) {
// Selection
const handleClick = (e: React.MouseEvent) => {
- if (isEditing) return
+ if (isEditing) {
+return
+}
e.preventDefault()
e.stopPropagation()
@@ -93,13 +103,19 @@ export function Note({ note }: NoteProps) {
// Navigation
const handleDoubleClick = (e: React.MouseEvent) => {
- if (isEditing) return
+ if (isEditing) {
+return
+}
e.stopPropagation()
if (note.content.trim()) {
const words = note.content.trim().split(/\s+/)
if (words.length > 0) {
- const spaceId = words[0].toLowerCase().replace(/[^a-z0-9]/g, '-')
+ const firstWord = words[0]
+ if (!firstWord) {
+return
+}
+ const spaceId = firstWord.toLowerCase().replace(/[^a-z0-9]/g, '-')
navigateToSpace(spaceId)
}
}
@@ -107,7 +123,9 @@ export function Note({ note }: NoteProps) {
// Drag and drop
const handleMouseDown = (e: React.MouseEvent) => {
- if (isEditing) return
+ if (isEditing) {
+return
+}
e.preventDefault()
e.stopPropagation()
@@ -122,7 +140,9 @@ export function Note({ note }: NoteProps) {
}
const handleMouseMove = (e: MouseEvent) => {
- if (!isDragging) return
+ if (!isDragging) {
+return
+}
const deltaX = e.clientX - dragStart.x
const deltaY = e.clientY - dragStart.y
@@ -162,6 +182,7 @@ export function Note({ note }: NoteProps) {
document.removeEventListener('mouseup', handleMouseUp)
}
}
+ return undefined
}, [isDragging, dragStart])
if (isEditing) {
diff --git a/src/components/Space.tsx b/src/components/Space.tsx
index e0a056f..c20b962 100644
--- a/src/components/Space.tsx
+++ b/src/components/Space.tsx
@@ -49,7 +49,9 @@ export function Space() {
// Drag in empty space - rectangle selection
const handleMouseDown = (e: React.MouseEvent) => {
- if (e.detail === 2) return // Ignore double-click
+ if (e.detail === 2) {
+return
+} // Ignore double-click
const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX - rect.left
@@ -61,10 +63,14 @@ export function Space() {
}
const handleMouseMove = (e: MouseEvent) => {
- if (!isSelecting) return
+ if (!isSelecting) {
+return
+}
const spaceElement = document.querySelector('[data-space-canvas]') as HTMLElement
- if (!spaceElement) return
+ if (!spaceElement) {
+return
+}
const rect = spaceElement.getBoundingClientRect()
const x = e.clientX - rect.left
@@ -74,13 +80,17 @@ export function Space() {
}
const handleMouseUp = (e: MouseEvent) => {
- if (!isSelecting) return
+ if (!isSelecting) {
+return
+}
setIsSelecting(false)
// Get final mouse position directly from event
const spaceElement = document.querySelector('[data-space-canvas]') as HTMLElement
- if (!spaceElement) return
+ if (!spaceElement) {
+return
+}
const rect = spaceElement.getBoundingClientRect()
const finalX = e.clientX - rect.left
@@ -114,13 +124,16 @@ export function Space() {
document.removeEventListener('mouseup', handleMouseUp)
}
}
+ return undefined
}, [isSelecting, selectionStart])
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Don't handle shortcuts if editing a note, navigation console or welcome modal is open
- if ($editingNoteId || showNavigationConsole || showWelcomeModal) return
+ if ($editingNoteId || showNavigationConsole || showWelcomeModal) {
+return
+}
if (e.key === ':' && !e.shiftKey) {
e.preventDefault()
diff --git a/src/components/WelcomeModal.tsx b/src/components/WelcomeModal.tsx
index 92336e7..1f2781c 100644
--- a/src/components/WelcomeModal.tsx
+++ b/src/components/WelcomeModal.tsx
@@ -21,7 +21,9 @@ export function WelcomeModal({ isOpen, onClose }: WelcomeModalProps) {
}
}
- if (!isOpen) return null
+ if (!isOpen) {
+return null
+}
return (
{
- if (!this.db) await this.init()
+ if (!this.db) {
+await this.init()
+}
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['spaces'], 'readonly')
const store = transaction.objectStore('spaces')
@@ -59,7 +61,9 @@ class IndexedDBAdapter implements StorageAdapter {
}
async saveSpace(space: Space): Promise {
- if (!this.db) await this.init()
+ if (!this.db) {
+await this.init()
+}
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['spaces'], 'readwrite')
const store = transaction.objectStore('spaces')
@@ -97,7 +101,7 @@ class IndexedDBAdapter implements StorageAdapter {
if (noteIndex === -1) {
throw new Error(`Note ${noteId} not found in space ${spaceId}`)
}
- space.notes[noteIndex] = { ...space.notes[noteIndex], ...updates }
+ space.notes[noteIndex] = { ...space.notes[noteIndex]!, ...updates } as Note
await this.saveSpace(space)
}
@@ -111,7 +115,9 @@ class IndexedDBAdapter implements StorageAdapter {
}
async getAllSpaceIds(): Promise {
- if (!this.db) await this.init()
+ if (!this.db) {
+await this.init()
+}
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['spaces'], 'readonly')
const store = transaction.objectStore('spaces')
@@ -123,7 +129,9 @@ class IndexedDBAdapter implements StorageAdapter {
}
async deleteSpace(id: string): Promise {
- if (!this.db) await this.init()
+ if (!this.db) {
+await this.init()
+}
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['spaces'], 'readwrite')
const store = transaction.objectStore('spaces')
@@ -136,7 +144,7 @@ class IndexedDBAdapter implements StorageAdapter {
}
class DatabaseAdapter implements StorageAdapter {
- private apiUrl: string
+ private readonly apiUrl: string
constructor(apiUrl?: string) {
this.apiUrl = apiUrl || this.getApiUrl()
@@ -155,8 +163,12 @@ class DatabaseAdapter implements StorageAdapter {
async getSpace(id: string): Promise {
const response = await fetch(`${this.apiUrl}/spaces/${id}`)
- if (response.status === 404) return undefined
- if (!response.ok) throw new Error(`Failed to get space: ${response.statusText}`)
+ if (response.status === 404) {
+return undefined
+}
+ if (!response.ok) {
+throw new Error(`Failed to get space: ${response.statusText}`)
+}
return await response.json()
}
@@ -166,7 +178,9 @@ class DatabaseAdapter implements StorageAdapter {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(space)
})
- if (!response.ok) throw new Error(`Failed to save space: ${response.statusText}`)
+ if (!response.ok) {
+throw new Error(`Failed to save space: ${response.statusText}`)
+}
}
async createSpace(id: string): Promise {
@@ -181,7 +195,9 @@ class DatabaseAdapter implements StorageAdapter {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(note)
})
- if (!response.ok) throw new Error(`Failed to add note: ${response.statusText}`)
+ if (!response.ok) {
+throw new Error(`Failed to add note: ${response.statusText}`)
+}
}
async updateNoteInSpace(spaceId: string, noteId: string, updates: Partial): Promise {
@@ -190,19 +206,25 @@ class DatabaseAdapter implements StorageAdapter {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
})
- if (!response.ok) throw new Error(`Failed to update note: ${response.statusText}`)
+ if (!response.ok) {
+throw new Error(`Failed to update note: ${response.statusText}`)
+}
}
async deleteNoteFromSpace(spaceId: string, noteId: string): Promise {
const response = await fetch(`${this.apiUrl}/spaces/${spaceId}/notes/${noteId}`, {
method: 'DELETE'
})
- if (!response.ok) throw new Error(`Failed to delete note: ${response.statusText}`)
+ if (!response.ok) {
+throw new Error(`Failed to delete note: ${response.statusText}`)
+}
}
async getAllSpaceIds(): Promise {
const response = await fetch(`${this.apiUrl}/spaces`)
- if (!response.ok) throw new Error(`Failed to get spaces: ${response.statusText}`)
+ if (!response.ok) {
+throw new Error(`Failed to get spaces: ${response.statusText}`)
+}
const spaces = await response.json()
return spaces.map((space: Space) => space.id)
}
@@ -211,7 +233,9 @@ class DatabaseAdapter implements StorageAdapter {
const response = await fetch(`${this.apiUrl}/spaces/${id}`, {
method: 'DELETE'
})
- if (!response.ok) throw new Error(`Failed to delete space: ${response.statusText}`)
+ if (!response.ok) {
+throw new Error(`Failed to delete space: ${response.statusText}`)
+}
}
}
diff --git a/src/store.ts b/src/store.ts
index 5d8682f..4947d6a 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -2,14 +2,14 @@ import { atom } from 'nanostores'
import { db } from './db'
import { decompressSpace } from './utils/shareSpace'
-type Note = {
+interface Note {
id: string
x: number
y: number
content: string
}
-type Space = {
+interface Space {
id: string
notes: Note[]
}
@@ -93,13 +93,17 @@ export const goBack = async () => {
const newHistory = history.slice(0, -1)
navigationHistory.set(newHistory)
const previousSpaceId = newHistory[newHistory.length - 1]
- await loadSpace(previousSpaceId)
+ if (previousSpaceId !== undefined) {
+ await loadSpace(previousSpaceId)
+ }
}
}
export const createNote = async (x: number, y: number) => {
const space = currentSpace.get()
- if (!space) return null
+ if (!space) {
+return null
+}
const note: Note = {
id: `note-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
@@ -117,7 +121,9 @@ export const createNote = async (x: number, y: number) => {
export const updateNote = async (noteId: string, updates: Partial) => {
const space = currentSpace.get()
- if (!space) return
+ if (!space) {
+return
+}
await db.updateNoteInSpace(space.id, noteId, updates)
const updatedSpace = await db.getSpace(space.id)
@@ -126,7 +132,9 @@ export const updateNote = async (noteId: string, updates: Partial) => {
export const deleteNote = async (noteId: string) => {
const space = currentSpace.get()
- if (!space) return
+ if (!space) {
+return
+}
await db.deleteNoteFromSpace(space.id, noteId)
const updatedSpace = await db.getSpace(space.id)
@@ -145,7 +153,7 @@ export const getSpaceFromUrl = () => {
}
// Selection management
-export const selectNote = (noteId: string, multiSelect: boolean = false) => {
+export const selectNote = (noteId: string, multiSelect = false) => {
const currentSelection = selectedNoteIds.get()
if (multiSelect) {
@@ -163,7 +171,9 @@ export const selectNote = (noteId: string, multiSelect: boolean = false) => {
export const selectNotesInRect = (minX: number, minY: number, maxX: number, maxY: number) => {
const space = currentSpace.get()
- if (!space) return
+ if (!space) {
+return
+}
const selectedIds = space.notes
.filter(note => {
@@ -186,7 +196,9 @@ export const clearSelection = () => {
export const deleteSelectedNotes = async () => {
const selectedIds = selectedNoteIds.get()
const space = currentSpace.get()
- if (!space || selectedIds.length === 0) return
+ if (!space || selectedIds.length === 0) {
+return
+}
for (const noteId of selectedIds) {
await db.deleteNoteFromSpace(space.id, noteId)
@@ -200,7 +212,9 @@ export const deleteSelectedNotes = async () => {
export const moveSelectedNotesLocal = (deltaX: number, deltaY: number) => {
const selectedIds = selectedNoteIds.get()
const space = currentSpace.get()
- if (!space || selectedIds.length === 0) return
+ if (!space || selectedIds.length === 0) {
+return
+}
// Update positions locally without DB calls
const updatedNotes = space.notes.map(note => {
@@ -216,7 +230,9 @@ export const moveSelectedNotesLocal = (deltaX: number, deltaY: number) => {
export const saveSelectedNotesToDB = async () => {
const selectedIds = selectedNoteIds.get()
const space = currentSpace.get()
- if (!space || selectedIds.length === 0) return
+ if (!space || selectedIds.length === 0) {
+return
+}
// Save to DB
for (const noteId of selectedIds) {
diff --git a/tsconfig.app.json b/tsconfig.app.json
index 227a6c6..5ac0514 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -21,7 +21,21 @@
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
+ "noUncheckedSideEffectImports": true,
+
+ /* Additional strict checks */
+ "noImplicitAny": true,
+ "strictNullChecks": true,
+ "strictFunctionTypes": true,
+ "strictBindCallApply": true,
+ "strictPropertyInitialization": true,
+ "noImplicitThis": true,
+ "alwaysStrict": true,
+ "noImplicitReturns": true,
+ "noUncheckedIndexedAccess": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "exactOptionalPropertyTypes": true,
+ "forceConsistentCasingInFileNames": true
},
"include": ["src"]
}
diff --git a/tsconfig.node.json b/tsconfig.node.json
index f85a399..5448547 100644
--- a/tsconfig.node.json
+++ b/tsconfig.node.json
@@ -19,7 +19,21 @@
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
+ "noUncheckedSideEffectImports": true,
+
+ /* Additional strict checks */
+ "noImplicitAny": true,
+ "strictNullChecks": true,
+ "strictFunctionTypes": true,
+ "strictBindCallApply": true,
+ "strictPropertyInitialization": true,
+ "noImplicitThis": true,
+ "alwaysStrict": true,
+ "noImplicitReturns": true,
+ "noUncheckedIndexedAccess": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "exactOptionalPropertyTypes": true,
+ "forceConsistentCasingInFileNames": true
},
"include": ["vite.config.ts"]
}