From 9d4d465755d0993789d8864a38fff5969ddecf2a Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 7 Mar 2026 09:51:57 -0600 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20ReadmeModal=20=E2=80=94=20full=20RE?= =?UTF-8?q?ADME=20rendered=20in=20a=20themed=20slide-in=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/ReadmeModal.jsx | 451 ++++++++++++++++++++++++++ 1 file changed, 451 insertions(+) create mode 100644 client/src/components/ReadmeModal.jsx diff --git a/client/src/components/ReadmeModal.jsx b/client/src/components/ReadmeModal.jsx new file mode 100644 index 0000000..e2c4588 --- /dev/null +++ b/client/src/components/ReadmeModal.jsx @@ -0,0 +1,451 @@ +import React, { useEffect, useRef } from 'react'; + +// ─── Minimal Markdown → HTML renderer ──────────────────────────────────────── +// Handles: headings, bold, inline-code, fenced code blocks, tables, hr, +// unordered lists, ordered lists, and paragraphs. +function mdToHtml(md) { + const lines = md.split('\n'); + const out = []; + let i = 0; + let inUl = false; + let inOl = false; + let inTable = false; + let tableHead = false; + + const closeOpenLists = () => { + if (inUl) { out.push(''); inUl = false; } + if (inOl) { out.push(''); inOl = false; } + if (inTable) { out.push(''); inTable = false; tableHead = false; } + }; + + const inline = (s) => + s + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/`([^`]+)`/g, '$1'); + + while (i < lines.length) { + const line = lines[i]; + + // Fenced code block + if (line.startsWith('```')) { + closeOpenLists(); + const lang = line.slice(3).trim(); + const codeLines = []; + i++; + while (i < lines.length && !lines[i].startsWith('```')) { + codeLines.push(lines[i].replace(/&/g,'&').replace(//g,'>')); + i++; + } + out.push(`
${codeLines.join('\n')}
`); + i++; + continue; + } + + // HR + if (/^---+$/.test(line.trim())) { + closeOpenLists(); + out.push('
'); + i++; + continue; + } + + // Headings + const hMatch = line.match(/^(#{1,4})\s+(.+)/); + if (hMatch) { + closeOpenLists(); + const level = hMatch[1].length; + const id = hMatch[2].toLowerCase().replace(/[^a-z0-9]+/g, '-'); + out.push(`${inline(hMatch[2])}`); + i++; + continue; + } + + // Table row + if (line.trim().startsWith('|')) { + const cells = line.trim().replace(/^\||\|$/g, '').split('|').map(c => c.trim()); + if (!inTable) { + closeOpenLists(); + inTable = true; + tableHead = true; + out.push(''); + cells.forEach(c => out.push(``)); + out.push(''); + i++; + // skip separator row + if (i < lines.length && lines[i].trim().startsWith('|') && /^[\|\s\-:]+$/.test(lines[i])) i++; + continue; + } else { + out.push(''); + cells.forEach(c => out.push(``)); + out.push(''); + i++; + continue; + } + } + + // Unordered list + const ulMatch = line.match(/^[-*]\s+(.*)/); + if (ulMatch) { + if (inTable) closeOpenLists(); + if (!inUl) { if (inOl) { out.push(''); inOl = false; } out.push(''); inUl = false; } out.push('
    '); inOl = true; } + out.push(`
  1. ${inline(olMatch[1])}
  2. `); + i++; + continue; + } + + // Blank line + if (line.trim() === '') { + closeOpenLists(); + i++; + continue; + } + + // Paragraph + closeOpenLists(); + out.push(`

    ${inline(line)}

    `); + i++; + } + + closeOpenLists(); + return out.join('\n'); +} + +// ─── Styles ─────────────────────────────────────────────────────────────────── +const overlay = { + position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)', + zIndex: 2000, display: 'flex', alignItems: 'flex-start', justifyContent: 'flex-end', +}; + +const panel = { + background: '#111217', color: '#f8f9fa', width: '760px', maxWidth: '95vw', + height: '100vh', overflowY: 'auto', boxShadow: '-4px 0 32px rgba(0,0,0,0.8)', + display: 'flex', flexDirection: 'column', +}; + +const header = { + background: 'linear-gradient(135deg, #000000, #151622)', color: 'white', + padding: '22px 28px', position: 'sticky', top: 0, zIndex: 10, + borderBottom: '1px solid #222', display: 'flex', alignItems: 'center', + justifyContent: 'space-between', +}; + +const closeBtn = { + background: 'none', border: 'none', color: 'white', + fontSize: '22px', cursor: 'pointer', lineHeight: 1, +}; + +const body = { + padding: '28px 32px', flex: 1, fontSize: '13px', lineHeight: '1.7', +}; + +// Injected + +
    e.stopPropagation()}> + + {/* Header */} +
    +
    +
    + 📋 CPAS Tracker — Documentation +
    +
    + Admin reference · use Esc or click outside to close +
    +
    + +
    + + {/* TOC strip */} +
    + {toc.filter(h => h.level <= 2).map((h) => ( + + ))} +
    + + {/* Body */} +
    + + {/* Footer */} +
    + CPAS Violation Tracker · internal admin use only +
    +
    +
    + ); +} From d4638783a4b49c3a38eea12a6bb3102ac43629c5 Mon Sep 17 00:00:00 2001 From: jason Date: Sat, 7 Mar 2026 09:52:16 -0600 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20add=20Docs=20button=20to=20navbar?= =?UTF-8?q?=20=E2=80=94=20opens=20ReadmeModal=20slide-in=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.jsx | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index 6fdac9c..8639fb6 100755 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import ViolationForm from './components/ViolationForm'; import Dashboard from './components/Dashboard'; +import ReadmeModal from './components/ReadmeModal'; const tabs = [ { id: 'dashboard', label: '📊 Dashboard' }, @@ -20,11 +21,29 @@ const s = { cursor: 'pointer', fontWeight: active ? 700 : 400, fontSize: '14px', background: 'none', border: 'none', }), + // Docs button sits flush-right in the nav + docsBtn: { + marginLeft: 'auto', + background: 'none', + border: '1px solid #2a2b3a', + color: '#9ca0b8', + borderRadius: '6px', + padding: '6px 14px', + fontSize: '12px', + cursor: 'pointer', + fontWeight: 600, + letterSpacing: '0.3px', + display: 'flex', + alignItems: 'center', + gap: '6px', + }, card: { maxWidth: '1100px', margin: '30px auto', background: '#111217', borderRadius: '10px', boxShadow: '0 2px 16px rgba(0,0,0,0.6)', border: '1px solid #222' }, }; export default function App() { - const [tab, setTab] = useState('dashboard'); + const [tab, setTab] = useState('dashboard'); + const [showReadme, setShowReadme] = useState(false); + return (
    + {tabs.map(t => ( ))} + + +
    {tab === 'dashboard' ? : }
    + + {showReadme && setShowReadme(false)} />} ); }
${inline(c)}
${inline(c)}