Stricter compilation rules, better linting
This commit is contained in:
10
.prettierrc.json
Normal file
10
.prettierrc.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
@ -12,12 +12,92 @@ export default tseslint.config([
|
|||||||
extends: [
|
extends: [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommended,
|
||||||
|
tseslint.configs.strict,
|
||||||
|
tseslint.configs.stylistic,
|
||||||
reactHooks.configs['recommended-latest'],
|
reactHooks.configs['recommended-latest'],
|
||||||
reactRefresh.configs.vite,
|
reactRefresh.configs.vite,
|
||||||
],
|
],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
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'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"typecheck": "tsc -b --noEmit",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -52,7 +52,9 @@ export function NavigationConsole({ isOpen, onClose }: NavigationConsoleProps) {
|
|||||||
}, [input, existingSpaces])
|
}, [input, existingSpaces])
|
||||||
|
|
||||||
const handleShare = async () => {
|
const handleShare = async () => {
|
||||||
if (!space) return
|
if (!space) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const shareLink = generateShareLink(space)
|
const shareLink = generateShareLink(space)
|
||||||
const success = await copyToClipboard(shareLink)
|
const success = await copyToClipboard(shareLink)
|
||||||
@ -109,7 +111,9 @@ export function NavigationConsole({ isOpen, onClose }: NavigationConsoleProps) {
|
|||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isOpen) return null
|
if (!isOpen) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from 'react'
|
|||||||
import { updateNote, deleteNote, editingNoteId, navigateToSpace, selectedNoteIds, selectNote, moveSelectedNotesLocal, saveSelectedNotesToDB, existingSpaceIds } from '../store'
|
import { updateNote, deleteNote, editingNoteId, navigateToSpace, selectedNoteIds, selectNote, moveSelectedNotesLocal, saveSelectedNotesToDB, existingSpaceIds } from '../store'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
|
|
||||||
type NoteType = {
|
interface NoteType {
|
||||||
id: string
|
id: string
|
||||||
x: number
|
x: number
|
||||||
y: 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
|
// Check if note content is a single word that matches an existing space
|
||||||
const isSpaceLink = () => {
|
const isSpaceLink = () => {
|
||||||
if (!note.content) return false
|
if (!note.content) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
const words = note.content.trim().split(/\s+/)
|
const words = note.content.trim().split(/\s+/)
|
||||||
if (words.length !== 1) return false
|
if (words.length !== 1) {
|
||||||
const spaceId = words[0].toLowerCase().replace(/[^a-z0-9]/g, '-')
|
return false
|
||||||
|
}
|
||||||
|
const firstWord = words[0]
|
||||||
|
if (!firstWord) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const spaceId = firstWord.toLowerCase().replace(/[^a-z0-9]/g, '-')
|
||||||
return $existingSpaceIds.includes(spaceId)
|
return $existingSpaceIds.includes(spaceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +80,9 @@ export function Note({ note }: NoteProps) {
|
|||||||
|
|
||||||
// Selection
|
// Selection
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
if (isEditing) return
|
if (isEditing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
@ -93,13 +103,19 @@ export function Note({ note }: NoteProps) {
|
|||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
const handleDoubleClick = (e: React.MouseEvent) => {
|
const handleDoubleClick = (e: React.MouseEvent) => {
|
||||||
if (isEditing) return
|
if (isEditing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
if (note.content.trim()) {
|
if (note.content.trim()) {
|
||||||
const words = note.content.trim().split(/\s+/)
|
const words = note.content.trim().split(/\s+/)
|
||||||
if (words.length > 0) {
|
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)
|
navigateToSpace(spaceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,7 +123,9 @@ export function Note({ note }: NoteProps) {
|
|||||||
|
|
||||||
// Drag and drop
|
// Drag and drop
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
const handleMouseDown = (e: React.MouseEvent) => {
|
||||||
if (isEditing) return
|
if (isEditing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
@ -122,7 +140,9 @@ export function Note({ note }: NoteProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
if (!isDragging) return
|
if (!isDragging) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const deltaX = e.clientX - dragStart.x
|
const deltaX = e.clientX - dragStart.x
|
||||||
const deltaY = e.clientY - dragStart.y
|
const deltaY = e.clientY - dragStart.y
|
||||||
@ -162,6 +182,7 @@ export function Note({ note }: NoteProps) {
|
|||||||
document.removeEventListener('mouseup', handleMouseUp)
|
document.removeEventListener('mouseup', handleMouseUp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return undefined
|
||||||
}, [isDragging, dragStart])
|
}, [isDragging, dragStart])
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
|
|||||||
@ -49,7 +49,9 @@ export function Space() {
|
|||||||
|
|
||||||
// Drag in empty space - rectangle selection
|
// Drag in empty space - rectangle selection
|
||||||
const handleMouseDown = (e: React.MouseEvent) => {
|
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 rect = e.currentTarget.getBoundingClientRect()
|
||||||
const x = e.clientX - rect.left
|
const x = e.clientX - rect.left
|
||||||
@ -61,10 +63,14 @@ export function Space() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
if (!isSelecting) return
|
if (!isSelecting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const spaceElement = document.querySelector('[data-space-canvas]') as HTMLElement
|
const spaceElement = document.querySelector('[data-space-canvas]') as HTMLElement
|
||||||
if (!spaceElement) return
|
if (!spaceElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const rect = spaceElement.getBoundingClientRect()
|
const rect = spaceElement.getBoundingClientRect()
|
||||||
const x = e.clientX - rect.left
|
const x = e.clientX - rect.left
|
||||||
@ -74,13 +80,17 @@ export function Space() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleMouseUp = (e: MouseEvent) => {
|
const handleMouseUp = (e: MouseEvent) => {
|
||||||
if (!isSelecting) return
|
if (!isSelecting) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setIsSelecting(false)
|
setIsSelecting(false)
|
||||||
|
|
||||||
// Get final mouse position directly from event
|
// Get final mouse position directly from event
|
||||||
const spaceElement = document.querySelector('[data-space-canvas]') as HTMLElement
|
const spaceElement = document.querySelector('[data-space-canvas]') as HTMLElement
|
||||||
if (!spaceElement) return
|
if (!spaceElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const rect = spaceElement.getBoundingClientRect()
|
const rect = spaceElement.getBoundingClientRect()
|
||||||
const finalX = e.clientX - rect.left
|
const finalX = e.clientX - rect.left
|
||||||
@ -114,13 +124,16 @@ export function Space() {
|
|||||||
document.removeEventListener('mouseup', handleMouseUp)
|
document.removeEventListener('mouseup', handleMouseUp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return undefined
|
||||||
}, [isSelecting, selectionStart])
|
}, [isSelecting, selectionStart])
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
// Don't handle shortcuts if editing a note, navigation console or welcome modal is open
|
// 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) {
|
if (e.key === ':' && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|||||||
@ -21,7 +21,9 @@ export function WelcomeModal({ isOpen, onClose }: WelcomeModalProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isOpen) return null
|
if (!isOpen) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
56
src/db.ts
56
src/db.ts
@ -1,11 +1,11 @@
|
|||||||
type Note = {
|
interface Note {
|
||||||
id: string
|
id: string
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Space = {
|
interface Space {
|
||||||
id: string
|
id: string
|
||||||
notes: Note[]
|
notes: Note[]
|
||||||
}
|
}
|
||||||
@ -47,7 +47,9 @@ class IndexedDBAdapter implements StorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSpace(id: string): Promise<Space | undefined> {
|
async getSpace(id: string): Promise<Space | undefined> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) {
|
||||||
|
await this.init()
|
||||||
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = this.db!.transaction(['spaces'], 'readonly')
|
const transaction = this.db!.transaction(['spaces'], 'readonly')
|
||||||
const store = transaction.objectStore('spaces')
|
const store = transaction.objectStore('spaces')
|
||||||
@ -59,7 +61,9 @@ class IndexedDBAdapter implements StorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveSpace(space: Space): Promise<void> {
|
async saveSpace(space: Space): Promise<void> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) {
|
||||||
|
await this.init()
|
||||||
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = this.db!.transaction(['spaces'], 'readwrite')
|
const transaction = this.db!.transaction(['spaces'], 'readwrite')
|
||||||
const store = transaction.objectStore('spaces')
|
const store = transaction.objectStore('spaces')
|
||||||
@ -97,7 +101,7 @@ class IndexedDBAdapter implements StorageAdapter {
|
|||||||
if (noteIndex === -1) {
|
if (noteIndex === -1) {
|
||||||
throw new Error(`Note ${noteId} not found in space ${spaceId}`)
|
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)
|
await this.saveSpace(space)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +115,9 @@ class IndexedDBAdapter implements StorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAllSpaceIds(): Promise<string[]> {
|
async getAllSpaceIds(): Promise<string[]> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) {
|
||||||
|
await this.init()
|
||||||
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = this.db!.transaction(['spaces'], 'readonly')
|
const transaction = this.db!.transaction(['spaces'], 'readonly')
|
||||||
const store = transaction.objectStore('spaces')
|
const store = transaction.objectStore('spaces')
|
||||||
@ -123,7 +129,9 @@ class IndexedDBAdapter implements StorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteSpace(id: string): Promise<void> {
|
async deleteSpace(id: string): Promise<void> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) {
|
||||||
|
await this.init()
|
||||||
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const transaction = this.db!.transaction(['spaces'], 'readwrite')
|
const transaction = this.db!.transaction(['spaces'], 'readwrite')
|
||||||
const store = transaction.objectStore('spaces')
|
const store = transaction.objectStore('spaces')
|
||||||
@ -136,7 +144,7 @@ class IndexedDBAdapter implements StorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DatabaseAdapter implements StorageAdapter {
|
class DatabaseAdapter implements StorageAdapter {
|
||||||
private apiUrl: string
|
private readonly apiUrl: string
|
||||||
|
|
||||||
constructor(apiUrl?: string) {
|
constructor(apiUrl?: string) {
|
||||||
this.apiUrl = apiUrl || this.getApiUrl()
|
this.apiUrl = apiUrl || this.getApiUrl()
|
||||||
@ -155,8 +163,12 @@ class DatabaseAdapter implements StorageAdapter {
|
|||||||
|
|
||||||
async getSpace(id: string): Promise<Space | undefined> {
|
async getSpace(id: string): Promise<Space | undefined> {
|
||||||
const response = await fetch(`${this.apiUrl}/spaces/${id}`)
|
const response = await fetch(`${this.apiUrl}/spaces/${id}`)
|
||||||
if (response.status === 404) return undefined
|
if (response.status === 404) {
|
||||||
if (!response.ok) throw new Error(`Failed to get space: ${response.statusText}`)
|
return undefined
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to get space: ${response.statusText}`)
|
||||||
|
}
|
||||||
return await response.json()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +178,9 @@ class DatabaseAdapter implements StorageAdapter {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(space)
|
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<Space> {
|
async createSpace(id: string): Promise<Space> {
|
||||||
@ -181,7 +195,9 @@ class DatabaseAdapter implements StorageAdapter {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(note)
|
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<Note>): Promise<void> {
|
async updateNoteInSpace(spaceId: string, noteId: string, updates: Partial<Note>): Promise<void> {
|
||||||
@ -190,19 +206,25 @@ class DatabaseAdapter implements StorageAdapter {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(updates)
|
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<void> {
|
async deleteNoteFromSpace(spaceId: string, noteId: string): Promise<void> {
|
||||||
const response = await fetch(`${this.apiUrl}/spaces/${spaceId}/notes/${noteId}`, {
|
const response = await fetch(`${this.apiUrl}/spaces/${spaceId}/notes/${noteId}`, {
|
||||||
method: 'DELETE'
|
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<string[]> {
|
async getAllSpaceIds(): Promise<string[]> {
|
||||||
const response = await fetch(`${this.apiUrl}/spaces`)
|
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()
|
const spaces = await response.json()
|
||||||
return spaces.map((space: Space) => space.id)
|
return spaces.map((space: Space) => space.id)
|
||||||
}
|
}
|
||||||
@ -211,7 +233,9 @@ class DatabaseAdapter implements StorageAdapter {
|
|||||||
const response = await fetch(`${this.apiUrl}/spaces/${id}`, {
|
const response = await fetch(`${this.apiUrl}/spaces/${id}`, {
|
||||||
method: 'DELETE'
|
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}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
38
src/store.ts
38
src/store.ts
@ -2,14 +2,14 @@ import { atom } from 'nanostores'
|
|||||||
import { db } from './db'
|
import { db } from './db'
|
||||||
import { decompressSpace } from './utils/shareSpace'
|
import { decompressSpace } from './utils/shareSpace'
|
||||||
|
|
||||||
type Note = {
|
interface Note {
|
||||||
id: string
|
id: string
|
||||||
x: number
|
x: number
|
||||||
y: number
|
y: number
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Space = {
|
interface Space {
|
||||||
id: string
|
id: string
|
||||||
notes: Note[]
|
notes: Note[]
|
||||||
}
|
}
|
||||||
@ -93,13 +93,17 @@ export const goBack = async () => {
|
|||||||
const newHistory = history.slice(0, -1)
|
const newHistory = history.slice(0, -1)
|
||||||
navigationHistory.set(newHistory)
|
navigationHistory.set(newHistory)
|
||||||
const previousSpaceId = newHistory[newHistory.length - 1]
|
const previousSpaceId = newHistory[newHistory.length - 1]
|
||||||
await loadSpace(previousSpaceId)
|
if (previousSpaceId !== undefined) {
|
||||||
|
await loadSpace(previousSpaceId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createNote = async (x: number, y: number) => {
|
export const createNote = async (x: number, y: number) => {
|
||||||
const space = currentSpace.get()
|
const space = currentSpace.get()
|
||||||
if (!space) return null
|
if (!space) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const note: Note = {
|
const note: Note = {
|
||||||
id: `note-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
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<Note>) => {
|
export const updateNote = async (noteId: string, updates: Partial<Note>) => {
|
||||||
const space = currentSpace.get()
|
const space = currentSpace.get()
|
||||||
if (!space) return
|
if (!space) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await db.updateNoteInSpace(space.id, noteId, updates)
|
await db.updateNoteInSpace(space.id, noteId, updates)
|
||||||
const updatedSpace = await db.getSpace(space.id)
|
const updatedSpace = await db.getSpace(space.id)
|
||||||
@ -126,7 +132,9 @@ export const updateNote = async (noteId: string, updates: Partial<Note>) => {
|
|||||||
|
|
||||||
export const deleteNote = async (noteId: string) => {
|
export const deleteNote = async (noteId: string) => {
|
||||||
const space = currentSpace.get()
|
const space = currentSpace.get()
|
||||||
if (!space) return
|
if (!space) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await db.deleteNoteFromSpace(space.id, noteId)
|
await db.deleteNoteFromSpace(space.id, noteId)
|
||||||
const updatedSpace = await db.getSpace(space.id)
|
const updatedSpace = await db.getSpace(space.id)
|
||||||
@ -145,7 +153,7 @@ export const getSpaceFromUrl = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Selection management
|
// Selection management
|
||||||
export const selectNote = (noteId: string, multiSelect: boolean = false) => {
|
export const selectNote = (noteId: string, multiSelect = false) => {
|
||||||
const currentSelection = selectedNoteIds.get()
|
const currentSelection = selectedNoteIds.get()
|
||||||
|
|
||||||
if (multiSelect) {
|
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) => {
|
export const selectNotesInRect = (minX: number, minY: number, maxX: number, maxY: number) => {
|
||||||
const space = currentSpace.get()
|
const space = currentSpace.get()
|
||||||
if (!space) return
|
if (!space) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const selectedIds = space.notes
|
const selectedIds = space.notes
|
||||||
.filter(note => {
|
.filter(note => {
|
||||||
@ -186,7 +196,9 @@ export const clearSelection = () => {
|
|||||||
export const deleteSelectedNotes = async () => {
|
export const deleteSelectedNotes = async () => {
|
||||||
const selectedIds = selectedNoteIds.get()
|
const selectedIds = selectedNoteIds.get()
|
||||||
const space = currentSpace.get()
|
const space = currentSpace.get()
|
||||||
if (!space || selectedIds.length === 0) return
|
if (!space || selectedIds.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for (const noteId of selectedIds) {
|
for (const noteId of selectedIds) {
|
||||||
await db.deleteNoteFromSpace(space.id, noteId)
|
await db.deleteNoteFromSpace(space.id, noteId)
|
||||||
@ -200,7 +212,9 @@ export const deleteSelectedNotes = async () => {
|
|||||||
export const moveSelectedNotesLocal = (deltaX: number, deltaY: number) => {
|
export const moveSelectedNotesLocal = (deltaX: number, deltaY: number) => {
|
||||||
const selectedIds = selectedNoteIds.get()
|
const selectedIds = selectedNoteIds.get()
|
||||||
const space = currentSpace.get()
|
const space = currentSpace.get()
|
||||||
if (!space || selectedIds.length === 0) return
|
if (!space || selectedIds.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Update positions locally without DB calls
|
// Update positions locally without DB calls
|
||||||
const updatedNotes = space.notes.map(note => {
|
const updatedNotes = space.notes.map(note => {
|
||||||
@ -216,7 +230,9 @@ export const moveSelectedNotesLocal = (deltaX: number, deltaY: number) => {
|
|||||||
export const saveSelectedNotesToDB = async () => {
|
export const saveSelectedNotesToDB = async () => {
|
||||||
const selectedIds = selectedNoteIds.get()
|
const selectedIds = selectedNoteIds.get()
|
||||||
const space = currentSpace.get()
|
const space = currentSpace.get()
|
||||||
if (!space || selectedIds.length === 0) return
|
if (!space || selectedIds.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Save to DB
|
// Save to DB
|
||||||
for (const noteId of selectedIds) {
|
for (const noteId of selectedIds) {
|
||||||
|
|||||||
@ -21,7 +21,21 @@
|
|||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": 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"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,21 @@
|
|||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": 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"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user