possibilité de partage un espace
This commit is contained in:
@ -1,5 +1,3 @@
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { navigationHistory, goBack } from '../store'
|
||||
|
||||
export function Navigation() {
|
||||
return null
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { navigateToSpace } from '../store'
|
||||
import { navigateToSpace, currentSpace } from '../store'
|
||||
import { db } from '../db'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { generateShareLink, copyToClipboard } from '../utils/shareSpace'
|
||||
|
||||
interface NavigationConsoleProps {
|
||||
isOpen: boolean
|
||||
@ -12,7 +14,9 @@ export function NavigationConsole({ isOpen, onClose }: NavigationConsoleProps) {
|
||||
const [existingSpaces, setExistingSpaces] = useState<string[]>([])
|
||||
const [filteredSpaces, setFilteredSpaces] = useState<string[]>([])
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const [isSharing, setIsSharing] = useState(false)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const space = useStore(currentSpace)
|
||||
|
||||
useEffect(() => {
|
||||
const loadSpaces = async () => {
|
||||
@ -47,11 +51,32 @@ export function NavigationConsole({ isOpen, onClose }: NavigationConsoleProps) {
|
||||
setSelectedIndex(0)
|
||||
}, [input, existingSpaces])
|
||||
|
||||
const handleShare = async () => {
|
||||
if (!space) return
|
||||
|
||||
const shareLink = generateShareLink(space)
|
||||
const success = await copyToClipboard(shareLink)
|
||||
|
||||
if (success) {
|
||||
setIsSharing(true)
|
||||
setTimeout(() => {
|
||||
onClose()
|
||||
setIsSharing(false)
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose()
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
|
||||
if (input.toLowerCase() === 'share') {
|
||||
handleShare()
|
||||
return
|
||||
}
|
||||
|
||||
const targetSpace = filteredSpaces[selectedIndex] || input
|
||||
if (targetSpace.trim()) {
|
||||
navigateToSpace(sanitizeSpaceId(targetSpace.trim()))
|
||||
@ -116,24 +141,45 @@ export function NavigationConsole({ isOpen, onClose }: NavigationConsoleProps) {
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '8px' }}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Name a space"
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
color: 'white',
|
||||
{isSharing ? (
|
||||
<div style={{
|
||||
color: '#4CAF50',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'monospace'
|
||||
}}
|
||||
/>
|
||||
fontFamily: 'monospace',
|
||||
padding: '4px 0'
|
||||
}}>
|
||||
✓ Link copied to clipboard!
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Name a space"
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
color: 'white',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'monospace'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{filteredSpaces.length > 0 && (
|
||||
{input.toLowerCase() === 'share' && (
|
||||
<div style={{
|
||||
borderTop: '1px solid #333',
|
||||
padding: '12px',
|
||||
color: '#4CAF50',
|
||||
fontSize: '13px'
|
||||
}}>
|
||||
Press Enter to share the current space.
|
||||
</div>
|
||||
)}
|
||||
{filteredSpaces.length > 0 && input.toLowerCase() !== 'share' && (
|
||||
<div style={{ borderTop: '1px solid #333', maxHeight: '200px', overflowY: 'auto' }}>
|
||||
{filteredSpaces.map((space, index) => {
|
||||
const isExisting = existingSpaces.includes(space)
|
||||
|
||||
@ -27,7 +27,7 @@ export function Space() {
|
||||
}, [])
|
||||
|
||||
// Click in empty space - clear selection (but not if we just finished a rectangle selection)
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
const handleClick = () => {
|
||||
if (justFinishedSelection) {
|
||||
setJustFinishedSelection(false)
|
||||
return
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
|
||||
interface WelcomeModalProps {
|
||||
isOpen: boolean
|
||||
@ -75,9 +75,12 @@ export function WelcomeModal({ isOpen, onClose }: WelcomeModalProps) {
|
||||
<p style={{ margin: '0 0 12px 0' }}>
|
||||
<strong>Warping:</strong> Double click on a note to warp between spaces.
|
||||
</p>
|
||||
<p style={{ margin: '0' }}>
|
||||
<p style={{ margin: '0 0 12px 0' }}>
|
||||
<strong>Delete :</strong> Backspace/Del to delete a note.
|
||||
</p>
|
||||
<p style={{ margin: '0' }}>
|
||||
<strong>Share:</strong> Type <code style={{ background: '#333', padding: '2px 4px', borderRadius: '2px' }}>share</code> in the nav console to share a space through a browser link (overrides the target).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
|
||||
37
src/store.ts
37
src/store.ts
@ -1,5 +1,6 @@
|
||||
import { atom } from 'nanostores'
|
||||
import { db } from './db'
|
||||
import { decompressSpace } from './utils/shareSpace'
|
||||
|
||||
type Note = {
|
||||
id: string
|
||||
@ -34,6 +35,36 @@ export const loadSpace = async (spaceId: string) => {
|
||||
}
|
||||
|
||||
currentSpace.set(null)
|
||||
|
||||
// Check if it's a shared space link
|
||||
if (spaceId.startsWith('shared/')) {
|
||||
const compressed = spaceId.substring(7) // Remove 'shared/' prefix
|
||||
const decompressed = decompressSpace(compressed)
|
||||
|
||||
if (decompressed) {
|
||||
// Save the shared space with a new ID
|
||||
const newSpaceId = `imported-${Date.now()}`
|
||||
let space = await db.createSpace(newSpaceId)
|
||||
|
||||
// Add all notes from the shared space
|
||||
for (const note of decompressed.notes) {
|
||||
await db.addNoteToSpace(newSpaceId, note)
|
||||
}
|
||||
|
||||
// Reload the space to get the updated version
|
||||
space = (await db.getSpace(newSpaceId))!
|
||||
currentSpace.set(space)
|
||||
currentSpaceId.set(newSpaceId)
|
||||
window.history.pushState({}, '', `/#${newSpaceId}`)
|
||||
|
||||
await refreshExistingSpaces()
|
||||
return space
|
||||
} else {
|
||||
// If decompression fails, go to crossroad
|
||||
spaceId = 'crossroad'
|
||||
}
|
||||
}
|
||||
|
||||
let space = await db.getSpace(spaceId)
|
||||
if (!space) {
|
||||
space = await db.createSpace(spaceId)
|
||||
@ -104,6 +135,12 @@ export const deleteNote = async (noteId: string) => {
|
||||
|
||||
export const getSpaceFromUrl = () => {
|
||||
const hash = window.location.hash.slice(1)
|
||||
|
||||
// Check if it's a shared space link
|
||||
if (hash.startsWith('shared/')) {
|
||||
return hash
|
||||
}
|
||||
|
||||
return hash || 'crossroad'
|
||||
}
|
||||
|
||||
|
||||
45
src/utils/shareSpace.ts
Normal file
45
src/utils/shareSpace.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import type { Space } from '../db'
|
||||
|
||||
export function compressSpace(space: Space): string {
|
||||
const minified = {
|
||||
id: space.id,
|
||||
notes: space.notes.map(note => ({
|
||||
id: note.id,
|
||||
x: note.x,
|
||||
y: note.y,
|
||||
content: note.content
|
||||
}))
|
||||
}
|
||||
|
||||
const jsonString = JSON.stringify(minified)
|
||||
const base64 = btoa(encodeURIComponent(jsonString))
|
||||
return base64
|
||||
}
|
||||
|
||||
export function decompressSpace(compressed: string): Space | null {
|
||||
try {
|
||||
const jsonString = decodeURIComponent(atob(compressed))
|
||||
const space = JSON.parse(jsonString)
|
||||
return space
|
||||
} catch (e) {
|
||||
console.error('Failed to decompress space:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function generateShareLink(space: Space): string {
|
||||
const compressed = compressSpace(space)
|
||||
const currentUrl = new URL(window.location.href)
|
||||
currentUrl.hash = `shared/${compressed}`
|
||||
return currentUrl.toString()
|
||||
}
|
||||
|
||||
export async function copyToClipboard(text: string): Promise<boolean> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user