/ concept-collection / mdshare
Sign in
concept-collection / mdshare
mdshare / src / App.tsx
117 lines · 4.1 KBBlameHistoryRaw
1import { useCallback, useEffect, useRef, useState } from 'react'
2import CodeMirrorEditor from './CodeMirrorEditor'
3import Preview from './Preview'
4import { encodeDocument, decodeDocument } from './url'
5import sampleDoc from './sample.md?raw'
6import './App.css'
8function readHash(): string | null {
9 const h = window.location.hash.replace(/^#/, '')
10 if (!h) return null
11 try {
12 return decodeDocument(h)
13 } catch {
14 return null
15 }
18export default function App() {
19 const [text, setText] = useState<string>(() => readHash() ?? sampleDoc)
20 const [urlLength, setUrlLength] = useState<number>(() => window.location.href.length)
21 const [copied, setCopied] = useState(false)
23 // Debounced text -> URL. replaceState (not assignment) so typing doesn't
24 // flood the browser history; lastWrittenRef lets the hashchange listener
25 // tell our own writes apart from external navigation.
26 const lastWrittenRef = useRef<string | null>(null)
27 useEffect(() => {
28 const t = setTimeout(() => {
29 const encoded = text ? encodeDocument(text) : ''
30 lastWrittenRef.current = encoded
31 const url = new URL(window.location.href)
32 url.hash = encoded
33 window.history.replaceState(null, '', url)
34 setUrlLength(url.href.length)
35 }, 300)
36 return () => clearTimeout(t)
37 }, [text])
39 // URL -> text, when the user pastes a different mdshare link into the
40 // address bar or navigates back/forward.
41 useEffect(() => {
42 const onHashChange = () => {
43 const h = window.location.hash.replace(/^#/, '')
44 if (h === lastWrittenRef.current) return
45 const decoded = readHash()
46 if (decoded !== null) setText(decoded)
47 }
48 window.addEventListener('hashchange', onHashChange)
49 return () => window.removeEventListener('hashchange', onHashChange)
50 }, [])
52 const copyLink = useCallback(async () => {
53 // Encode the current text directly so a fast type-then-copy can't race
54 // the debounced URL write.
55 const url = new URL(window.location.href)
56 url.hash = text ? encodeDocument(text) : ''
57 lastWrittenRef.current = url.hash.replace(/^#/, '')
58 window.history.replaceState(null, '', url)
59 setUrlLength(url.href.length)
60 await navigator.clipboard.writeText(url.href)
61 setCopied(true)
62 setTimeout(() => setCopied(false), 1500)
63 }, [text])
65 const newDocument = useCallback(() => {
66 if (text.trim() && text !== sampleDoc) {
67 if (!window.confirm('Discard the current document? (The old link keeps working.)')) return
68 }
69 setText('')
70 }, [text])
72 // Simple draggable splitter
73 const [split, setSplit] = useState(0.5)
74 const mainRef = useRef<HTMLDivElement>(null)
75 const onSplitterDown = useCallback((e: React.PointerEvent) => {
76 e.preventDefault()
77 const main = mainRef.current
78 if (!main) return
79 const rect = main.getBoundingClientRect()
80 const onMove = (ev: PointerEvent) => {
81 const f = (ev.clientX - rect.left) / rect.width
82 setSplit(Math.min(0.8, Math.max(0.2, f)))
83 }
84 const onUp = () => {
85 window.removeEventListener('pointermove', onMove)
86 window.removeEventListener('pointerup', onUp)
87 }
88 window.addEventListener('pointermove', onMove)
89 window.addEventListener('pointerup', onUp)
90 }, [])
92 return (
93 <div className="app">
94 <header className="topbar">
95 <span className="title">mdshare</span>
96 <span className="subtitle">markdown that lives in the URL</span>
97 <span className="spacer" />
98 <span className="url-size" title="Length of the shareable URL">
99 URL: {urlLength.toLocaleString()} chars
100 </span>
101 <button onClick={newDocument}>New</button>
102 <button className="primary" onClick={copyLink}>
103 {copied ? 'Copied!' : 'Copy link'}
104 </button>
105 </header>
106 <main className="panes" ref={mainRef}>
107 <section className="pane" style={{ flexBasis: `${split * 100}%` }}>
108 <CodeMirrorEditor value={text} onChange={setText} />
109 </section>
110 <div className="splitter" onPointerDown={onSplitterDown} />
111 <section className="pane pane-preview">
112 <Preview markdown={text} />
113 </section>
114 </main>
115 </div>
116 )
moveopenescclose