mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
13956804c2
- 5-table schema (journeys, entries, photos, trips, contributors) with migrations 87-91 - Trip-to-Journey sync engine with skeleton entries and photo sync - Full CRUD API for journeys, entries, photos with Immich/Synology integration - Timeline, Gallery and Map views with entry editor (markdown, mood, weather, pros/cons) - Journey frontpage with hero card, stats and trip suggestions - Public share links with token-based access and photo proxy - PDF photo book export (Polarsteps-inspired) - Dashboard redesign: mobile greeting, live trip hero, quick actions, unified card design - BottomNav profile sheet with settings/admin/logout - DayPlan mobile inline place picker - TripFormModal members management - Vacay calendar trip date indicator dots - Fix contributor photo access (403) for journey Immich/Synology photos - Trip deletion cleanup for journey skeleton entries - i18n: 231 new keys across all 14 languages (native translations, no fallbacks)
82 lines
3.0 KiB
TypeScript
82 lines
3.0 KiB
TypeScript
import { Bold, Italic, Heading2, Link, Quote, List, ListOrdered, Minus } from 'lucide-react'
|
|
|
|
interface Props {
|
|
textareaRef: React.RefObject<HTMLTextAreaElement | null>
|
|
onUpdate: (value: string) => void
|
|
dark?: boolean
|
|
}
|
|
|
|
type FormatAction = { type: 'wrap'; before: string; after: string } | { type: 'line'; prefix: string }
|
|
|
|
const ACTIONS: Array<{ icon: typeof Bold; label: string; action: FormatAction }> = [
|
|
{ icon: Bold, label: 'Bold', action: { type: 'wrap', before: '**', after: '**' } },
|
|
{ icon: Italic, label: 'Italic', action: { type: 'wrap', before: '_', after: '_' } },
|
|
{ icon: Heading2, label: 'Heading', action: { type: 'line', prefix: '## ' } },
|
|
{ icon: Quote, label: 'Quote', action: { type: 'line', prefix: '> ' } },
|
|
{ icon: Link, label: 'Link', action: { type: 'wrap', before: '[', after: '](url)' } },
|
|
{ icon: List, label: 'List', action: { type: 'line', prefix: '- ' } },
|
|
{ icon: ListOrdered, label: 'Ordered', action: { type: 'line', prefix: '1. ' } },
|
|
{ icon: Minus, label: 'Divider', action: { type: 'line', prefix: '\n---\n' } },
|
|
]
|
|
|
|
export default function MarkdownToolbar({ textareaRef, onUpdate, dark }: Props) {
|
|
const apply = (action: FormatAction) => {
|
|
const ta = textareaRef.current
|
|
if (!ta) return
|
|
|
|
const start = ta.selectionStart
|
|
const end = ta.selectionEnd
|
|
const text = ta.value
|
|
const selected = text.slice(start, end)
|
|
|
|
let result: string
|
|
let cursorPos: number
|
|
|
|
if (action.type === 'wrap') {
|
|
result = text.slice(0, start) + action.before + selected + action.after + text.slice(end)
|
|
cursorPos = selected ? end + action.before.length + action.after.length : start + action.before.length
|
|
} else {
|
|
// line prefix — find start of current line
|
|
const lineStart = text.lastIndexOf('\n', start - 1) + 1
|
|
result = text.slice(0, lineStart) + action.prefix + text.slice(lineStart)
|
|
cursorPos = start + action.prefix.length
|
|
}
|
|
|
|
onUpdate(result)
|
|
|
|
// restore cursor after React re-render
|
|
requestAnimationFrame(() => {
|
|
ta.focus()
|
|
ta.setSelectionRange(cursorPos, cursorPos)
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div style={{
|
|
display: 'flex', gap: 2, padding: '6px 4px',
|
|
borderBottom: `1px solid var(--journal-border)`,
|
|
overflowX: 'auto',
|
|
}}>
|
|
{ACTIONS.map(a => (
|
|
<button
|
|
key={a.label}
|
|
type="button"
|
|
title={a.label}
|
|
onClick={() => apply(a.action)}
|
|
style={{
|
|
width: 32, height: 32, borderRadius: 6,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
background: 'none', border: 'none',
|
|
color: 'var(--journal-muted)', cursor: 'pointer',
|
|
flexShrink: 0,
|
|
}}
|
|
onMouseEnter={e => e.currentTarget.style.background = dark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}
|
|
onMouseLeave={e => e.currentTarget.style.background = 'none'}
|
|
>
|
|
<a.icon size={15} />
|
|
</button>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|