From 55713902ed29bfc89e9222db149e5dd5782af468 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 12 May 2026 18:38:57 +0200 Subject: [PATCH] Make MyNote draggable --- frontend/src/components/MyNote.jsx | 125 ++++++++++++++++++++++++++++- frontend/src/index.css | 15 +++- 2 files changed, 135 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/MyNote.jsx b/frontend/src/components/MyNote.jsx index 6fb0c89..b827ed1 100644 --- a/frontend/src/components/MyNote.jsx +++ b/frontend/src/components/MyNote.jsx @@ -1,11 +1,21 @@ import { useEffect, useRef, useState } from 'react' import api from '../api/client' +const TAB_POSITION_KEY = 'pedshub_mynote_tab_position' + function formatSavedAt(value) { if (!value) return 'Not saved yet' return `Saved ${new Date(value).toLocaleString()}` } +function clampPosition(x, y, width, height) { + const margin = 8 + return { + x: Math.min(Math.max(margin, x), Math.max(margin, window.innerWidth - width - margin)), + y: Math.min(Math.max(margin, y), Math.max(margin, window.innerHeight - height - margin)), + } +} + export default function MyNote({ variant = 'tab' }) { const [open, setOpen] = useState(false) const [content, setContent] = useState('') @@ -13,8 +23,13 @@ export default function MyNote({ variant = 'tab' }) { const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [error, setError] = useState('') + const [position, setPosition] = useState(null) + const [dragging, setDragging] = useState(false) + const tabRef = useRef(null) const saveRef = useRef(null) const loadedRef = useRef(false) + const dragRef = useRef(null) + const draggedRef = useRef(false) const loadNote = async () => { if (loadedRef.current) return @@ -36,6 +51,44 @@ export default function MyNote({ variant = 'tab' }) { if (variant === 'card') loadNote() }, [variant]) + useEffect(() => { + if (variant !== 'tab') return + const rect = tabRef.current?.getBoundingClientRect() + let next = null + try { + const saved = JSON.parse(localStorage.getItem(TAB_POSITION_KEY) || 'null') + if (saved && Number.isFinite(saved.x) && Number.isFinite(saved.y)) { + next = clampPosition(saved.x, saved.y, rect?.width || 120, rect?.height || 48) + } + } catch { + next = null + } + if (!next && rect) next = { x: rect.left, y: rect.top } + if (next) setPosition(next) + + const handleResize = () => { + setPosition(current => { + if (!current) return current + const resizedRect = tabRef.current?.getBoundingClientRect() + const clamped = clampPosition(current.x, current.y, resizedRect?.width || 120, resizedRect?.height || 48) + localStorage.setItem(TAB_POSITION_KEY, JSON.stringify(clamped)) + return clamped + }) + } + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, [variant]) + + useEffect(() => { + if (variant !== 'tab') return + setPosition(current => { + if (!current) return current + const rect = tabRef.current?.getBoundingClientRect() + if (!rect) return current + return clampPosition(current.x, current.y, rect.width, rect.height) + }) + }, [open, variant]) + useEffect(() => { if (!loadedRef.current) return clearTimeout(saveRef.current) @@ -59,16 +112,73 @@ export default function MyNote({ variant = 'tab' }) { loadNote() } + const startDrag = event => { + if (variant !== 'tab' || event.button !== 0) return + if (event.target.closest('button, textarea') && event.currentTarget !== event.target) return + const rect = tabRef.current?.getBoundingClientRect() + if (!rect) return + dragRef.current = { + pointerId: event.pointerId, + offsetX: event.clientX - rect.left, + offsetY: event.clientY - rect.top, + startX: event.clientX, + startY: event.clientY, + moved: false, + position: { x: rect.left, y: rect.top }, + } + event.currentTarget.setPointerCapture?.(event.pointerId) + } + + const moveDrag = event => { + const drag = dragRef.current + if (!drag || drag.pointerId !== event.pointerId) return + const rect = tabRef.current?.getBoundingClientRect() + if (!rect) return + const next = clampPosition(event.clientX - drag.offsetX, event.clientY - drag.offsetY, rect.width, rect.height) + drag.position = next + if (Math.abs(event.clientX - drag.startX) > 4 || Math.abs(event.clientY - drag.startY) > 4) { + drag.moved = true + setDragging(true) + } + setPosition(next) + event.preventDefault() + } + + const stopDrag = event => { + const drag = dragRef.current + if (!drag || drag.pointerId !== event.pointerId) return + if (drag.moved) { + localStorage.setItem(TAB_POSITION_KEY, JSON.stringify(drag.position)) + } + draggedRef.current = drag.moved + dragRef.current = null + setDragging(false) + setTimeout(() => { + draggedRef.current = false + }, 0) + } + + const openFromTabButton = event => { + if (draggedRef.current) { + event.preventDefault() + return + } + openNote() + } + const preview = content.trim() ? content.trim().slice(0, 150) + (content.trim().length > 150 ? '...' : '') : 'Your single study note will appear here once you start writing during quizzes.' const editor = (
-
+
MyNote
-
{saving ? 'Saving...' : formatSavedAt(updatedAt)}
+
{variant === 'tab' ? 'Drag this header to move. ' : ''}{saving ? 'Saving...' : formatSavedAt(updatedAt)}
{variant === 'tab' && }
@@ -105,9 +215,16 @@ export default function MyNote({ variant = 'tab' }) { } return ( -
+
{!open ? ( - + ) : editor}
) diff --git a/frontend/src/index.css b/frontend/src/index.css index 1105089..4bd564c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -304,6 +304,9 @@ body { bottom: 18px; z-index: 45; } +.mynote-tab.dragging { + user-select: none; +} .mynote-tab-button { border: 1px solid rgba(37, 99, 235, 0.22); border-radius: 999px; @@ -311,9 +314,10 @@ body { background: linear-gradient(135deg, #2563eb, #7c3aed); color: white; box-shadow: 0 16px 40px rgba(37, 99, 235, 0.28); - cursor: pointer; + cursor: grab; font-weight: 800; letter-spacing: -0.02em; + touch-action: none; } .mynote-editor { width: min(420px, calc(100vw - 28px)); @@ -333,6 +337,15 @@ body { padding: 14px 16px 10px; border-bottom: 1px solid var(--border); } +.mynote-drag-handle { + cursor: grab; + touch-action: none; + user-select: none; +} +.mynote-tab.dragging .mynote-tab-button, +.mynote-tab.dragging .mynote-drag-handle { + cursor: grabbing; +} .mynote-title { font-weight: 850; font-size: 1rem;