262 lines
7.5 KiB
TypeScript
262 lines
7.5 KiB
TypeScript
interface Note {
|
|
id: string
|
|
x: number
|
|
y: number
|
|
content: string
|
|
}
|
|
|
|
interface Space {
|
|
id: string
|
|
notes: Note[]
|
|
}
|
|
|
|
type StorageMode = 'indexeddb' | 'database'
|
|
|
|
interface StorageAdapter {
|
|
init(): Promise<void>
|
|
getSpace(id: string): Promise<Space | undefined>
|
|
saveSpace(space: Space): Promise<void>
|
|
createSpace(id: string): Promise<Space>
|
|
addNoteToSpace(spaceId: string, note: Note): Promise<void>
|
|
updateNoteInSpace(spaceId: string, noteId: string, updates: Partial<Note>): Promise<void>
|
|
deleteNoteFromSpace(spaceId: string, noteId: string): Promise<void>
|
|
getAllSpaceIds(): Promise<string[]>
|
|
deleteSpace(id: string): Promise<void>
|
|
}
|
|
|
|
class IndexedDBAdapter implements StorageAdapter {
|
|
private db: IDBDatabase | null = null
|
|
|
|
async init() {
|
|
return new Promise<void>((resolve, reject) => {
|
|
const request = indexedDB.open('palace-db', 1)
|
|
|
|
request.onerror = () => reject(request.error)
|
|
request.onsuccess = () => {
|
|
this.db = request.result
|
|
resolve()
|
|
}
|
|
|
|
request.onupgradeneeded = (event) => {
|
|
const db = (event.target as IDBOpenDBRequest).result
|
|
if (!db.objectStoreNames.contains('spaces')) {
|
|
db.createObjectStore('spaces', { keyPath: 'id' })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
async getSpace(id: string): Promise<Space | undefined> {
|
|
if (!this.db) {
|
|
await this.init()
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = this.db!.transaction(['spaces'], 'readonly')
|
|
const store = transaction.objectStore('spaces')
|
|
const request = store.get(id)
|
|
|
|
request.onerror = () => reject(request.error)
|
|
request.onsuccess = () => resolve(request.result)
|
|
})
|
|
}
|
|
|
|
async saveSpace(space: Space): Promise<void> {
|
|
if (!this.db) {
|
|
await this.init()
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = this.db!.transaction(['spaces'], 'readwrite')
|
|
const store = transaction.objectStore('spaces')
|
|
const request = store.put(space)
|
|
|
|
request.onerror = () => reject(request.error)
|
|
request.onsuccess = () => resolve()
|
|
})
|
|
}
|
|
|
|
async createSpace(id: string): Promise<Space> {
|
|
const space: Space = {
|
|
id,
|
|
notes: []
|
|
}
|
|
await this.saveSpace(space)
|
|
return space
|
|
}
|
|
|
|
async addNoteToSpace(spaceId: string, note: Note): Promise<void> {
|
|
const space = await this.getSpace(spaceId)
|
|
if (!space) {
|
|
throw new Error(`Space ${spaceId} not found`)
|
|
}
|
|
space.notes.push(note)
|
|
await this.saveSpace(space)
|
|
}
|
|
|
|
async updateNoteInSpace(spaceId: string, noteId: string, updates: Partial<Note>): Promise<void> {
|
|
const space = await this.getSpace(spaceId)
|
|
if (!space) {
|
|
throw new Error(`Space ${spaceId} not found`)
|
|
}
|
|
const noteIndex = space.notes.findIndex(n => n.id === noteId)
|
|
if (noteIndex === -1) {
|
|
throw new Error(`Note ${noteId} not found in space ${spaceId}`)
|
|
}
|
|
space.notes[noteIndex] = { ...space.notes[noteIndex]!, ...updates } as Note
|
|
await this.saveSpace(space)
|
|
}
|
|
|
|
async deleteNoteFromSpace(spaceId: string, noteId: string): Promise<void> {
|
|
const space = await this.getSpace(spaceId)
|
|
if (!space) {
|
|
throw new Error(`Space ${spaceId} not found`)
|
|
}
|
|
space.notes = space.notes.filter(n => n.id !== noteId)
|
|
await this.saveSpace(space)
|
|
}
|
|
|
|
async getAllSpaceIds(): Promise<string[]> {
|
|
if (!this.db) {
|
|
await this.init()
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = this.db!.transaction(['spaces'], 'readonly')
|
|
const store = transaction.objectStore('spaces')
|
|
const request = store.getAllKeys()
|
|
|
|
request.onerror = () => reject(request.error)
|
|
request.onsuccess = () => resolve(request.result as string[])
|
|
})
|
|
}
|
|
|
|
async deleteSpace(id: string): Promise<void> {
|
|
if (!this.db) {
|
|
await this.init()
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = this.db!.transaction(['spaces'], 'readwrite')
|
|
const store = transaction.objectStore('spaces')
|
|
const request = store.delete(id)
|
|
|
|
request.onerror = () => reject(request.error)
|
|
request.onsuccess = () => resolve()
|
|
})
|
|
}
|
|
}
|
|
|
|
class DatabaseAdapter implements StorageAdapter {
|
|
private readonly apiUrl: string
|
|
|
|
constructor(apiUrl?: string) {
|
|
this.apiUrl = apiUrl || this.getApiUrl()
|
|
}
|
|
|
|
private getApiUrl(): string {
|
|
if (typeof window !== 'undefined' && window.location.hostname === 'palace.raphaelforment.fr') {
|
|
return 'https://palace.raphaelforment.fr:3001/api'
|
|
}
|
|
return '/api'
|
|
}
|
|
|
|
async init(): Promise<void> {
|
|
// Database connection is handled per request
|
|
}
|
|
|
|
async getSpace(id: string): Promise<Space | undefined> {
|
|
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}`)
|
|
}
|
|
return await response.json()
|
|
}
|
|
|
|
async saveSpace(space: Space): Promise<void> {
|
|
const response = await fetch(`${this.apiUrl}/spaces/${space.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(space)
|
|
})
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to save space: ${response.statusText}`)
|
|
}
|
|
}
|
|
|
|
async createSpace(id: string): Promise<Space> {
|
|
const space: Space = { id, notes: [] }
|
|
await this.saveSpace(space)
|
|
return space
|
|
}
|
|
|
|
async addNoteToSpace(spaceId: string, note: Note): Promise<void> {
|
|
const response = await fetch(`${this.apiUrl}/spaces/${spaceId}/notes`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(note)
|
|
})
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to add note: ${response.statusText}`)
|
|
}
|
|
}
|
|
|
|
async updateNoteInSpace(spaceId: string, noteId: string, updates: Partial<Note>): Promise<void> {
|
|
const response = await fetch(`${this.apiUrl}/spaces/${spaceId}/notes/${noteId}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(updates)
|
|
})
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to update note: ${response.statusText}`)
|
|
}
|
|
}
|
|
|
|
async deleteNoteFromSpace(spaceId: string, noteId: string): Promise<void> {
|
|
const response = await fetch(`${this.apiUrl}/spaces/${spaceId}/notes/${noteId}`, {
|
|
method: 'DELETE'
|
|
})
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to delete note: ${response.statusText}`)
|
|
}
|
|
}
|
|
|
|
async getAllSpaceIds(): Promise<string[]> {
|
|
const response = await fetch(`${this.apiUrl}/spaces`)
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to get spaces: ${response.statusText}`)
|
|
}
|
|
const spaces = await response.json()
|
|
return spaces.map((space: Space) => space.id)
|
|
}
|
|
|
|
async deleteSpace(id: string): Promise<void> {
|
|
const response = await fetch(`${this.apiUrl}/spaces/${id}`, {
|
|
method: 'DELETE'
|
|
})
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to delete space: ${response.statusText}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
function getStorageMode(): StorageMode {
|
|
return (localStorage.getItem('palace-storage-mode') as StorageMode) || 'indexeddb'
|
|
}
|
|
|
|
function setStorageMode(mode: StorageMode): void {
|
|
localStorage.setItem('palace-storage-mode', mode)
|
|
}
|
|
|
|
function createStorageAdapter(): StorageAdapter {
|
|
const mode = getStorageMode()
|
|
switch (mode) {
|
|
case 'database':
|
|
return new DatabaseAdapter()
|
|
case 'indexeddb':
|
|
default:
|
|
return new IndexedDBAdapter()
|
|
}
|
|
}
|
|
|
|
export const db = createStorageAdapter()
|
|
export { type Note, type Space, type StorageMode, getStorageMode, setStorageMode } |