v0.6.2 added help window
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jama-frontend",
|
||||
"version": "0.5.1",
|
||||
"version": "0.6.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -16,7 +16,8 @@
|
||||
"@emoji-mart/data": "^1.1.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"date-fns": "^3.3.1"
|
||||
"date-fns": "^3.3.1",
|
||||
"marked": "^12.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
|
||||
71
frontend/src/components/HelpModal.jsx
Normal file
71
frontend/src/components/HelpModal.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { marked } from 'marked';
|
||||
import { api } from '../utils/api.js';
|
||||
|
||||
// Configure marked for safe rendering
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
|
||||
export default function HelpModal({ onClose, dismissed: initialDismissed }) {
|
||||
const [content, setContent] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [dismissed, setDismissed] = useState(!!initialDismissed);
|
||||
|
||||
useEffect(() => {
|
||||
api.getHelp()
|
||||
.then(({ content }) => setContent(content))
|
||||
.catch(() => setContent('# Getting Started\n\nHelp content could not be loaded.'))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleDismissToggle = async (e) => {
|
||||
const val = e.target.checked;
|
||||
setDismissed(val);
|
||||
try {
|
||||
await api.dismissHelp(val);
|
||||
if (val) onClose(); // immediately close when "do not show again" checked
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={e => e.target === e.currentTarget && onClose()}>
|
||||
<div className="modal help-modal">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between" style={{ marginBottom: 16 }}>
|
||||
<h2 className="modal-title" style={{ margin: 0 }}>Getting Started</h2>
|
||||
<button className="btn-icon" onClick={onClose}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Scrollable markdown content */}
|
||||
<div className="help-content">
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: 40, color: 'var(--text-tertiary)' }}>Loading…</div>
|
||||
) : (
|
||||
<div
|
||||
className="help-markdown"
|
||||
dangerouslySetInnerHTML={{ __html: marked.parse(content) }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="help-footer">
|
||||
<label className="flex items-center gap-2 text-sm" style={{ cursor: 'pointer', color: 'var(--text-secondary)' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={dismissed}
|
||||
onChange={handleDismissToggle}
|
||||
/>
|
||||
Do not show again at login
|
||||
</label>
|
||||
<button className="btn btn-primary btn-sm" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -148,7 +148,7 @@ export default function SettingsModal({ onClose }) {
|
||||
{settings.pw_reset_active === 'true' && (
|
||||
<div className="warning-banner">
|
||||
<span>⚠️</span>
|
||||
<span><strong>PW_RESET is active.</strong> The default admin password is being reset on every restart. Set PW_RESET=false in your environment variables to stop this.</span>
|
||||
<span><strong>ADMPW_RESET is active.</strong> The default admin password is being reset on every restart. Set ADMPW_RESET=false in your environment variables to stop this.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -43,7 +43,7 @@ function useAppSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifications, unreadGroups = new Map(), onNewChat, onProfile, onUsers, onSettings: onOpenSettings, onGroupsUpdated, isMobile, onAbout }) {
|
||||
export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifications, unreadGroups = new Map(), onNewChat, onProfile, onUsers, onSettings: onOpenSettings, onGroupsUpdated, isMobile, onAbout, onHelp }) {
|
||||
const { user, logout } = useAuth();
|
||||
const { connected } = useSocket();
|
||||
const toast = useToast();
|
||||
@@ -219,6 +219,10 @@ export default function Sidebar({ groups, activeGroupId, onSelectGroup, notifica
|
||||
</>
|
||||
)}
|
||||
<hr className="divider" style={{ margin: '4px 0' }} />
|
||||
<button className="footer-menu-item" onClick={() => { setShowMenu(false); onHelp && onHelp(); }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
Help
|
||||
</button>
|
||||
<button className="footer-menu-item" onClick={() => { setShowMenu(false); onAbout && onAbout(); }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||
About
|
||||
|
||||
@@ -399,3 +399,50 @@ a { color: inherit; text-decoration: none; }
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
/* ── Help Modal ─────────────────────────────────────────────── */
|
||||
.help-modal {
|
||||
width: min(720px, 94vw);
|
||||
max-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.help-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px 24px;
|
||||
background: var(--surface-variant);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.help-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
/* Markdown typography */
|
||||
.help-markdown h1 { font-size: 1.5rem; font-weight: 700; margin: 0 0 16px; color: var(--text-primary); }
|
||||
.help-markdown h2 { font-size: 1.15rem; font-weight: 700; margin: 24px 0 10px; color: var(--text-primary); border-bottom: 1px solid var(--border); padding-bottom: 4px; }
|
||||
.help-markdown h3 { font-size: 1rem; font-weight: 600; margin: 16px 0 6px; color: var(--text-primary); }
|
||||
.help-markdown p { margin: 0 0 12px; line-height: 1.65; color: var(--text-secondary); font-size: 14px; }
|
||||
.help-markdown ul, .help-markdown ol { margin: 0 0 12px 20px; color: var(--text-secondary); font-size: 14px; }
|
||||
.help-markdown li { margin-bottom: 4px; line-height: 1.6; }
|
||||
.help-markdown strong { font-weight: 600; color: var(--text-primary); }
|
||||
.help-markdown em { font-style: italic; }
|
||||
.help-markdown code { font-family: monospace; font-size: 13px; background: var(--background); padding: 1px 5px; border-radius: 4px; color: var(--primary); }
|
||||
.help-markdown pre { background: var(--background); border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 16px; overflow-x: auto; margin: 0 0 12px; }
|
||||
.help-markdown pre code { background: none; padding: 0; color: var(--text-primary); }
|
||||
.help-markdown blockquote { border-left: 3px solid var(--primary); margin: 0 0 12px; padding: 6px 14px; background: var(--primary-light); border-radius: 0 var(--radius) var(--radius) 0; }
|
||||
.help-markdown blockquote p { margin: 0; color: var(--text-secondary); }
|
||||
.help-markdown hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
|
||||
.help-markdown a { color: var(--primary); text-decoration: underline; }
|
||||
|
||||
[data-theme="dark"] .help-markdown code { background: var(--surface); }
|
||||
[data-theme="dark"] .help-markdown pre { background: var(--surface); }
|
||||
[data-theme="dark"] .help-markdown blockquote { background: rgba(99,102,241,0.1); }
|
||||
|
||||
@@ -11,6 +11,7 @@ import SettingsModal from '../components/SettingsModal.jsx';
|
||||
import NewChatModal from '../components/NewChatModal.jsx';
|
||||
import GlobalBar from '../components/GlobalBar.jsx';
|
||||
import AboutModal from '../components/AboutModal.jsx';
|
||||
import HelpModal from '../components/HelpModal.jsx';
|
||||
import './Chat.css';
|
||||
|
||||
function urlBase64ToUint8Array(base64String) {
|
||||
@@ -31,10 +32,21 @@ export default function Chat() {
|
||||
const [activeGroupId, setActiveGroupId] = useState(null);
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const [unreadGroups, setUnreadGroups] = useState(new Map());
|
||||
const [modal, setModal] = useState(null); // 'profile' | 'users' | 'settings' | 'newchat'
|
||||
const [modal, setModal] = useState(null); // 'profile' | 'users' | 'settings' | 'newchat' | 'help'
|
||||
const [helpDismissed, setHelpDismissed] = useState(true); // true until status loaded
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
|
||||
const [showSidebar, setShowSidebar] = useState(true);
|
||||
|
||||
// Check if help should be shown on login
|
||||
useEffect(() => {
|
||||
api.getHelpStatus()
|
||||
.then(({ dismissed }) => {
|
||||
setHelpDismissed(dismissed);
|
||||
if (!dismissed) setModal('help');
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handle = () => {
|
||||
const mobile = window.innerWidth < 768;
|
||||
@@ -210,6 +222,7 @@ export default function Chat() {
|
||||
onGroupsUpdated={loadGroups}
|
||||
isMobile={isMobile}
|
||||
onAbout={() => setModal('about')}
|
||||
onHelp={() => setModal('help')}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -228,6 +241,7 @@ export default function Chat() {
|
||||
{modal === 'settings' && <SettingsModal onClose={() => setModal(null)} />}
|
||||
{modal === 'newchat' && <NewChatModal onClose={() => setModal(null)} onCreated={(g) => { loadGroups(); setModal(null); setActiveGroupId(g.id); }} />}
|
||||
{modal === 'about' && <AboutModal onClose={() => setModal(null)} />}
|
||||
{modal === 'help' && <HelpModal onClose={() => setModal(null)} dismissed={helpDismissed} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function Login() {
|
||||
{settings.pw_reset_active === 'true' && (
|
||||
<div className="warning-banner" style={{ marginBottom: 16 }}>
|
||||
<span>⚠️</span>
|
||||
<span><strong>PW_RESET is enabled.</strong> The admin password is being reset on each restart. Disable PW_RESET in your environment to stop this behavior.</span>
|
||||
<span><strong>ADMPW_RESET is enabled.</strong> The admin password is being reset on each restart. Disable ADMPW_RESET in your environment to stop this behavior.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ export const api = {
|
||||
createGroup: (body) => req('POST', '/groups', body),
|
||||
renameGroup: (id, name) => req('PATCH', `/groups/${id}/rename`, { name }),
|
||||
setCustomGroupName: (id, name) => req('PATCH', `/groups/${id}/custom-name`, { name }),
|
||||
getHelp: () => req('GET', '/help'),
|
||||
getHelpStatus: () => req('GET', '/help/status'),
|
||||
dismissHelp: (dismissed) => req('POST', '/help/dismiss', { dismissed }),
|
||||
getMembers: (id) => req('GET', `/groups/${id}/members`),
|
||||
addMember: (groupId, userId) => req('POST', `/groups/${groupId}/members`, { userId }),
|
||||
removeMember: (groupId, userId) => req('DELETE', `/groups/${groupId}/members/${userId}`),
|
||||
|
||||
Reference in New Issue
Block a user