Skip to main content
Glama
northernvariables

FedMCP - Federal Parliamentary Information

NoteEditor.tsx9.99 kB
'use client'; import React, { useState, useCallback, useEffect } from 'react'; import { MarkdownRenderer } from './MarkdownRenderer'; import { getTierLimits, isNoteWithinLimit, getNoteLimitMessage, getUpgradeMessage, } from '@/lib/bookmarks/tierLimits'; interface NoteEditorProps { value: string; onChange: (value: string) => void; tier?: string | null; placeholder?: string; autoFocus?: boolean; minHeight?: string; showPreview?: boolean; className?: string; /** * Called when save is triggered (Cmd/Ctrl + S) */ onSave?: () => void; /** * Show character counter */ showCharCounter?: boolean; } /** * Markdown note editor with live preview and tier-aware character limits. * * Features: * - Split view: Edit | Preview * - Tier-aware character limits * - Keyboard shortcuts (Cmd+S to save, Cmd+B for bold, etc.) * - Auto-growing textarea * - Character counter with warnings * * @example * ```tsx * <NoteEditor * value={note} * onChange={setNote} * tier="BASIC" * onSave={handleSave} * /> * ``` */ export function NoteEditor({ value, onChange, tier, placeholder = 'Write your note in Markdown...', autoFocus = false, minHeight = '120px', showPreview: initialShowPreview = false, className = '', onSave, showCharCounter = true, }: NoteEditorProps) { const [showPreview, setShowPreview] = useState(initialShowPreview); const limits = getTierLimits(tier); // Check if notes are allowed for this tier if (!limits.hasNotes) { const upgradeMsg = getUpgradeMessage('notes', tier); return ( <div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600"> <div className="flex items-center gap-2 text-gray-600 dark:text-gray-400"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /> </svg> <span className="font-medium">{upgradeMsg.message}</span> </div> </div> ); } const charCount = value.length; const isOverLimit = !isNoteWithinLimit(charCount, tier); const limitMessage = getNoteLimitMessage(charCount, tier); // Calculate warning level for character counter const maxLength = limits.maxNoteLength; let warningLevel: 'normal' | 'warning' | 'danger' = 'normal'; if (maxLength !== null) { const percentage = (charCount / maxLength) * 100; if (percentage >= 100) warningLevel = 'danger'; else if (percentage >= 80) warningLevel = 'warning'; } // Handle keyboard shortcuts const handleKeyDown = useCallback( (e: React.KeyboardEvent<HTMLTextAreaElement>) => { // Cmd/Ctrl + S to save if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); onSave?.(); return; } // Cmd/Ctrl + B for bold if ((e.metaKey || e.ctrlKey) && e.key === 'b') { e.preventDefault(); insertMarkdown('**', '**', 'bold text'); return; } // Cmd/Ctrl + I for italic if ((e.metaKey || e.ctrlKey) && e.key === 'i') { e.preventDefault(); insertMarkdown('*', '*', 'italic text'); return; } // Cmd/Ctrl + K for link if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); insertMarkdown('[', '](url)', 'link text'); return; } }, [onSave] ); // Insert markdown formatting at cursor position const insertMarkdown = (before: string, after: string, placeholder: string) => { const textarea = document.querySelector('textarea') as HTMLTextAreaElement; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = value.substring(start, end); const textToInsert = selectedText || placeholder; const newValue = value.substring(0, start) + before + textToInsert + after + value.substring(end); onChange(newValue); // Set cursor position after inserted text setTimeout(() => { textarea.focus(); const newCursorPos = start + before.length + textToInsert.length; textarea.setSelectionRange(newCursorPos, newCursorPos); }, 0); }; return ( <div className={`note-editor ${className}`}> {/* Toolbar */} <div className="flex items-center justify-between gap-2 mb-2 pb-2 border-b border-gray-200 dark:border-gray-700"> {/* Formatting buttons */} <div className="flex items-center gap-1"> <button type="button" onClick={() => insertMarkdown('**', '**', 'bold')} className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-sm font-bold" title="Bold (Cmd+B)" > B </button> <button type="button" onClick={() => insertMarkdown('*', '*', 'italic')} className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-sm italic" title="Italic (Cmd+I)" > I </button> <button type="button" onClick={() => insertMarkdown('[', '](url)', 'link')} className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-sm" title="Link (Cmd+K)" > 🔗 </button> <button type="button" onClick={() => insertMarkdown('`', '`', 'code')} className="px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-xs font-mono" title="Inline code" > {'</>'} </button> <button type="button" onClick={() => { const textarea = document.querySelector('textarea') as HTMLTextAreaElement; const start = textarea.selectionStart; const lineStart = value.lastIndexOf('\n', start - 1) + 1; onChange(value.substring(0, lineStart) + '- ' + value.substring(lineStart)); }} className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-sm" title="Bullet list" > • </button> </div> {/* Preview toggle */} <button type="button" onClick={() => setShowPreview(!showPreview)} className={`px-3 py-1 text-sm rounded transition-colors ${ showPreview ? 'bg-red-600 text-white' : 'bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600' }`} > {showPreview ? 'Edit' : 'Preview'} </button> </div> {/* Editor / Preview */} {showPreview ? ( <div className="prose prose-sm max-w-none p-3 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900" style={{ minHeight }} > {value ? ( <MarkdownRenderer content={value} /> ) : ( <p className="text-gray-400 italic">Nothing to preview</p> )} </div> ) : ( <textarea value={value} onChange={(e) => onChange(e.target.value)} onKeyDown={handleKeyDown} placeholder={placeholder} autoFocus={autoFocus} className={`w-full p-3 border rounded-lg font-mono text-sm resize-y focus:outline-none focus:ring-2 transition-colors ${ isOverLimit ? 'border-red-500 focus:ring-red-500 bg-red-50 dark:bg-red-900/10' : 'border-gray-200 dark:border-gray-700 focus:ring-red-600 bg-white dark:bg-gray-900' }`} style={{ minHeight }} /> )} {/* Character counter */} {showCharCounter && ( <div className="flex items-center justify-between mt-2 text-xs"> <div className={`font-medium ${ warningLevel === 'danger' ? 'text-red-600 dark:text-red-400' : warningLevel === 'warning' ? 'text-amber-600 dark:text-amber-400' : 'text-gray-500 dark:text-gray-400' }`} > {limitMessage} </div> {/* Keyboard shortcuts hint */} <div className="text-gray-400 dark:text-gray-500"> <span className="hidden sm:inline"> Shortcuts: <kbd className="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded">⌘S</kbd> save,{' '} <kbd className="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded">⌘B</kbd> bold </span> </div> </div> )} {/* Over limit warning */} {isOverLimit && ( <div className="mt-2 p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded text-sm text-red-600 dark:text-red-400"> Your note exceeds the {maxLength?.toLocaleString()} character limit for {tier} tier.{' '} {tier === 'BASIC' && ( <button type="button" className="underline font-medium hover:text-red-700 dark:hover:text-red-300" onClick={() => { // TODO: Integrate with upgrade flow console.log('Upgrade to PRO'); }} > Upgrade to PRO for unlimited notes </button> )} </div> )} </div> ); } /** * Compact note editor for inline use (e.g., in modals) */ export function CompactNoteEditor(props: Omit<NoteEditorProps, 'showPreview'>) { return ( <NoteEditor {...props} minHeight="80px" showPreview={false} className="compact" /> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/northernvariables/FedMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server