possibilité de partage un espace

This commit is contained in:
2025-07-19 11:26:50 +02:00
parent 0b0d71768b
commit cb11398b3e
6 changed files with 151 additions and 22 deletions

View File

@ -1,5 +1,3 @@
import { useStore } from '@nanostores/react'
import { navigationHistory, goBack } from '../store'
export function Navigation() {
return null

View File

@ -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,6 +141,16 @@ export function NavigationConsole({ isOpen, onClose }: NavigationConsoleProps) {
}}
>
<div style={{ padding: '8px' }}>
{isSharing ? (
<div style={{
color: '#4CAF50',
fontSize: '14px',
fontFamily: 'monospace',
padding: '4px 0'
}}>
Link copied to clipboard!
</div>
) : (
<input
ref={inputRef}
value={input}
@ -132,8 +167,19 @@ export function NavigationConsole({ isOpen, onClose }: NavigationConsoleProps) {
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)

View File

@ -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

View File

@ -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' }}>

View File

@ -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
View 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
}
}