// Help & Support — the guides bundled with this version of Dashbread (Dashbread Pro swaps in the live // dashbread.com knowledge base), a developers' hooks reference, and Contact support (a Pro service; // Core points to the WordPress.org forum). Core itself contacts no other site. import { useEffect, useMemo, useRef, useState } from 'react'; import { __, _n, _x, sprintf } from '@wordpress/i18n'; import { cfg } from '../api.js'; import { Badge, Button, Card, Field, Icon, Notice, Spinner, toast } from '../ui.jsx'; async function call(path, opts = {}) { const res = await fetch(cfg.rest + path, { credentials: 'same-origin', headers: { 'X-WP-Nonce': cfg.nonce, 'Content-Type': 'application/json' }, ...opts }); const data = await res.json().catch(() => ({})); if (!res.ok) { const e = new Error(data?.message || sprintf( /* translators: %s: HTTP status code */ __('Request failed (%s)', 'dashbread'), res.status )); e.data = data?.data; throw e; } return data; } // One shortcode with one-click copy (and a second button for the closing tag of enclosing tags). function ShortcodeRow({ s }) { const [copied, setCopied] = useState(''); const copy = async (text, which) => { try { await navigator.clipboard.writeText(text); } catch { const t = document.createElement('textarea'); t.value = text; document.body.appendChild(t); t.select(); document.execCommand('copy'); t.remove(); } setCopied(which); setTimeout(() => setCopied(''), 1400); }; const tag = (text, which) => ( {text} ); return (
{tag(s.code, 'open')}{s.close ? tag(s.close, 'close') : null}{!s.available && s.requires ? {sprintf( /* translators: %s: required product or add-on name */ __('Needs %s', 'dashbread'), s.requires )} : null}
{s.note &&

{s.note}

}
); } function ShortcodeSections({ sections }) { return (
{sections.map((sec) => (
{sec.label}
{sec.note &&

{sec.note}

} {sec.items.map((s) => )}
))}
); } const scText = (s) => (s.code + ' ' + (s.close || '') + ' ' + (s.note || '')).toLowerCase(); function Article({ a, open, onToggle, onLoaded, codes = [], sections = null }) { const [body, setBody] = useState(a.body || ''); const [busy, setBusy] = useState(false); const ref = useRef(null); useEffect(() => { if (open && !body) { setBusy(true); call('admin/help/kb/' + a.slug).then((r) => { setBody(r.body); onLoaded && onLoaded(a.slug, r.body); }).catch(() => setBody('

' + __('This guide is not available. Read it on dashbread.com.', 'dashbread') + '

')).finally(() => setBusy(false)); } if (open && ref.current) ref.current.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); }, [open]); return (
{open &&
{sections && sections.length > 0 && } {!sections && codes.length > 0 &&
{_n('Shortcode', 'Shortcodes', codes.length, 'dashbread')}
{codes.map((s) => )}
} {busy ? :
} {__('Open on dashbread.com ↗', 'dashbread')}
}
); } export default function Help({ data }) { const [kb, setKb] = useState(null); const [err, setErr] = useState(''); const [q, setQ] = useState(''); const [cat, setCat] = useState(''); const [open, setOpen] = useState(() => new URLSearchParams(location.search).get('kb') || ''); const [subject, setSubject] = useState(''); const [msg, setMsg] = useState(''); const [sending, setSending] = useState(false); const [sent, setSent] = useState(null); useEffect(() => { call('admin/help/kb').then((r) => { setKb(r); cfg.helpUrl = r.help_url; }).catch((e) => setErr(e.message)); }, []); const route = data.status.route; const t = q.trim().toLowerCase(); // Shortcodes: every item, and the ones each guide owns. A search narrows the rows to what matched. const scSections = useMemo(() => (kb?.shortcodes || []).map((sec) => ({ ...sec, items: sec.items.filter((s) => !t || scText(s).includes(t)) })).filter((sec) => sec.items.length), [kb, t]); const scByGuide = useMemo(() => { const m = {}; scSections.forEach((sec) => sec.items.forEach((s) => { if (s.guide) (m[s.guide] = m[s.guide] || []).push(s); })); return m; }, [scSections]); const list = useMemo(() => { if (!kb) return []; return kb.articles.filter((a) => (!cat || a.category === cat) && (!t || (a.title + ' ' + a.excerpt).toLowerCase().includes(t) || (a.slug === 'shortcodes' ? scSections.length > 0 : (scByGuide[a.slug] || []).length > 0))); }, [kb, t, cat, scSections, scByGuide]); const hasScGuide = !!kb?.articles?.some((a) => a.slug === 'shortcodes'); const byCat = useMemo(() => { const m = {}; list.forEach((a) => { (m[a.category] = m[a.category] || []).push(a); }); return (kb?.categories || []).map((c) => [c, m[c.slug] || []]).filter(([, arr]) => arr.length); }, [list, kb]); const send = async () => { setSending(true); setErr(''); try { const r = await call('admin/help/contact', { method: 'POST', body: JSON.stringify({ subject, message: msg }) }); setSent(r.ticket || true); setSubject(''); setMsg(''); toast.show(__('Message sent', 'dashbread')); } catch (e) { setErr(e.message); } setSending(false); }; return (
{err && setErr('')}>{err}}

{__('Knowledge base', 'dashbread')}

setQ(e.target.value)} data-kb-search="1" />
{!kb ? : kb.articles.length === 0 ? (

{__('No guides are available here.', 'dashbread')} {/* translators: followed by a link to dashbread.com/help */ __('The full knowledge base is at', 'dashbread')} dashbread.com/help.

) : ( <>
{kb.categories.map((c) => )}
{byCat.length === 0 &&

{sprintf( /* translators: %s: search terms */ __('Nothing matches “%s”.', 'dashbread'), q )} {/* translators: followed by the link text "search on dashbread.com" */ __('Try another word, or', 'dashbread')} {__('search on dashbread.com', 'dashbread')}.

} {byCat.map(([c, arr]) => ( {c.description &&

{c.description}

}
{arr.map((a) =>
setOpen(open === a.slug ? '' : a.slug)} codes={scByGuide[a.slug] || []} sections={a.slug === 'shortcodes' ? scSections : null} />)}
))} )} {kb?.bundled && kb.articles.length > 0 &&

{__('These guides ship with this version of Dashbread.', 'dashbread')} {/* translators: followed by a link to dashbread.com/help */ __('The latest versions are on', 'dashbread')} dashbread.com/help.

} {kb && !hasScGuide && scSections.length > 0 && (

{__('Paste these into any page or post (or a Shortcode block).', 'dashbread')} {/* translators: followed by the button name "Copy" */ __('Click', 'dashbread')} {_x('Copy', 'button', 'dashbread')} {/* translators: follows "Click Copy" */ __('next to a tag.', 'dashbread')}

)}

{__('Everything below works with Core alone.', 'dashbread')} {/* translators: followed by the guide name "Hooks & filters" */ __('Full reference: the', 'dashbread')} {/* translators: follows "Full reference: the Hooks & filters" */ __('guide above.', 'dashbread')} {/* translators: followed by the link text "Shortcode reference" */ __('Page builders: the', 'dashbread')} .

{`// Register a native page (renders inside the shell at /${route}/reports)
add_action( 'dashbread_register', function ( $r ) {
    $r->register_page( 'reports', [
        'title'    => 'Reports',
        'icon'     => 'chart-line',        // any Lucide icon name
        'position' => 40,
        'render'   => fn() => '
Hello
', ] ); $r->register_notification_type( 'invoice', [ 'label' => 'Invoices', 'icon' => 'receipt' ] ); $r->register_header_item( 'jobs', [ 'icon' => 'briefcase', 'label' => 'Jobs', 'toggle' => 'flyout', 'tab' => 'jobs', 'badge' => fn() => 3 ] ); } ); // Notify a member (in-app; Pro adds email) dashbread_notify( $user_id, 'invoice', [ 'title' => 'Invoice #1042 is ready', 'url' => home_url( '/${route}/invoices/1042' ) ] ); // Add a screen under the Dashbread wp-admin menu, then mount it from your bundle: add_filter( 'dashbread_admin_screens', fn( $s ) => $s + [ '-jobs' => [ 'Jobs', 'jobs' ] ] ); // window.DashbreadAdmin.registerScreen( 'jobs', JobsComponent );`}
{kb?.contact ? (

{__('Priority email support — included with your Pro license.', 'dashbread')}

{sent && setSent(null)}>{typeof sent === 'string' && sent ? sprintf( /* translators: %s: support ticket number */ __('Sent — ticket %s.', 'dashbread'), sent ) : __('Sent.', 'dashbread')} {/* translators: followed by an email address */ __('We reply to', 'dashbread')} {cfg.email || ''}.} setSubject(e.target.value)} data-support-subject="1" />