From cb11398b3efea1ed7ea6e92cf21ebb00e713abaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sat, 19 Jul 2025 11:26:50 +0200 Subject: [PATCH] =?UTF-8?q?possibilit=C3=A9=20de=20partage=20un=20espace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Navigation.tsx | 2 - src/components/NavigationConsole.tsx | 80 ++++++++++++++++++++++------ src/components/Space.tsx | 2 +- src/components/WelcomeModal.tsx | 7 ++- src/store.ts | 37 +++++++++++++ src/utils/shareSpace.ts | 45 ++++++++++++++++ 6 files changed, 151 insertions(+), 22 deletions(-) create mode 100644 src/utils/shareSpace.ts diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index b549519..329bac7 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -1,5 +1,3 @@ -import { useStore } from '@nanostores/react' -import { navigationHistory, goBack } from '../store' export function Navigation() { return null diff --git a/src/components/NavigationConsole.tsx b/src/components/NavigationConsole.tsx index 33ca9f4..48ebc7a 100644 --- a/src/components/NavigationConsole.tsx +++ b/src/components/NavigationConsole.tsx @@ -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([]) const [filteredSpaces, setFilteredSpaces] = useState([]) const [selectedIndex, setSelectedIndex] = useState(0) + const [isSharing, setIsSharing] = useState(false) const inputRef = useRef(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) { }} >
- setInput(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Name a space" - style={{ - width: '100%', - background: 'transparent', - border: 'none', - outline: 'none', - color: 'white', + {isSharing ? ( +
+ fontFamily: 'monospace', + padding: '4px 0' + }}> + ✓ Link copied to clipboard! +
+ ) : ( + 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' + }} + /> + )}
- {filteredSpaces.length > 0 && ( + {input.toLowerCase() === 'share' && ( +
+ Press Enter to share the current space. +
+ )} + {filteredSpaces.length > 0 && input.toLowerCase() !== 'share' && (
{filteredSpaces.map((space, index) => { const isExisting = existingSpaces.includes(space) diff --git a/src/components/Space.tsx b/src/components/Space.tsx index b6eb9e5..e0a056f 100644 --- a/src/components/Space.tsx +++ b/src/components/Space.tsx @@ -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 diff --git a/src/components/WelcomeModal.tsx b/src/components/WelcomeModal.tsx index c01c9c6..92336e7 100644 --- a/src/components/WelcomeModal.tsx +++ b/src/components/WelcomeModal.tsx @@ -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) {

Warping: Double click on a note to warp between spaces.

-

+

Delete : Backspace/Del to delete a note.

+

+ Share: Type share in the nav console to share a space through a browser link (overrides the target). +

diff --git a/src/store.ts b/src/store.ts index 8bcf52e..5d8682f 100644 --- a/src/store.ts +++ b/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' } diff --git a/src/utils/shareSpace.ts b/src/utils/shareSpace.ts new file mode 100644 index 0000000..3a55a5a --- /dev/null +++ b/src/utils/shareSpace.ts @@ -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 { + try { + await navigator.clipboard.writeText(text) + return true + } catch (err) { + console.error('Failed to copy:', err) + return false + } +} \ No newline at end of file