json-object-editor
Version:
JOE the Json Object Editor | Platform Edition
479 lines (430 loc) • 20.6 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Harmonious Wellness — Protocol Report (Mockup)</title>
<style>
:root{
/* Brand-inspired (lightweight) */
--plum:#3a1430;
--gold:#c6a44d;
--olive:#6c7b3b;
--ink:#0f172a;
--muted:#475569;
--bg:#0b0810;
--paper:#ffffff;
--soft:#f8fafc;
--line:#e5e7eb;
--max: 980px;
--radius: 16px;
--font: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
}
html,body{height:100%;}
body{margin:0; font-family:var(--font); background:var(--bg); color:var(--ink);}
/* Topbar */
.topbar{position:sticky; top:0; z-index:10; background:rgba(11,8,16,.92); border-bottom:1px solid rgba(255,255,255,.10);}
.topbar .inner{max-width:var(--max); margin:0 auto; padding:10px 14px; display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;}
.brand{display:flex; align-items:center; gap:10px; color:#f1f5f9;}
.mark{width:28px; height:28px; border-radius:10px; background:linear-gradient(135deg,var(--olive),var(--gold)); box-shadow:0 0 0 3px rgba(198,164,77,.15);}
.brand b{letter-spacing:.2px;}
.actions{display:flex; gap:8px; flex-wrap:wrap;}
button{border:1px solid rgba(255,255,255,.18); background:rgba(255,255,255,.06); color:#e2e8f0; padding:8px 10px; border-radius:999px; font-weight:700; cursor:pointer;}
button.primary{border-color:rgba(198,164,77,.55); background:rgba(198,164,77,.18); color:#fff7e6;}
/* Page */
.wrap{max-width:var(--max); margin:18px auto 52px; padding:0 14px;}
.paper{background:var(--paper); border-radius:22px; overflow:hidden; border:1px solid rgba(255,255,255,.06);}
.hero{padding:18px; border-bottom:1px solid var(--line); background:linear-gradient(180deg,#fff,#fbfdff);}
.hero h1{margin:0; font-size:22px; line-height:1.2;}
.hero .sub{margin-top:6px; color:var(--muted); font-weight:700;}
.meta{display:flex; gap:10px; flex-wrap:wrap; margin-top:12px;}
.pill{display:inline-flex; gap:8px; align-items:center; padding:6px 10px; border:1px solid var(--line); background:var(--soft); border-radius:999px; font-weight:800; font-size:13px;}
.pill small{color:var(--muted); font-weight:800;}
.section{padding:16px 18px; border-bottom:1px solid var(--line);}
.section:last-child{border-bottom:none;}
.section h2{margin:0 0 10px; font-size:16px;}
.grid{display:grid; gap:10px;}
.two{grid-template-columns: repeat(2, minmax(0,1fr));}
@media (max-width:780px){.two{grid-template-columns:1fr;}}
.card{border:1px solid var(--line); border-radius:var(--radius); padding:12px; background:#fff;}
.card h3{margin:0 0 8px; font-size:13px; color:var(--plum);}
.muted{color:var(--muted); font-weight:650; line-height:1.55;}
ul{margin:0; padding-left:18px; color:var(--muted); font-weight:650;}
li{margin:6px 0;}
/* Phases */
.phase{border:1px solid var(--line); border-radius:18px; overflow:hidden; margin-bottom:12px;}
.phase .head{padding:12px; background:linear-gradient(180deg,#fff,#f9fbff); border-bottom:1px solid var(--line);}
.phase .head b{color:var(--plum);}
.phase .head p{margin:6px 0 0;}
.phase .recs{padding:12px; display:grid; gap:10px;}
.rec{border:1px solid var(--line); border-radius:14px; padding:10px;}
.rec-top{display:flex; justify-content:space-between; gap:10px; align-items:flex-start;}
.rec-name{font-weight:900;}
.dose{font-size:12px; font-weight:900; color:var(--plum); background:rgba(58,20,48,.06); border:1px solid rgba(58,20,48,.12); padding:6px 10px; border-radius:999px; white-space:nowrap;}
.more{display:none; margin-top:8px; padding-top:8px; border-top:1px dashed rgba(15,23,42,.18);}
.toggle{margin-left:8px; border:1px solid rgba(15,23,42,.18); background:#fff; color:var(--ink); padding:3px 8px; border-radius:999px; font-weight:900; font-size:12px;}
/* PDF Mode */
body.pdf-mode .toggle{display:none;}
body.pdf-mode .more{display:block;}
/* Print */
@media print{
body{background:#fff;}
.topbar{display:none;}
.wrap{max-width:100%; margin:0; padding:0;}
.paper{border-radius:0;}
.more{display:block ;}
.toggle{display:none ;}
.phase{break-inside:avoid; page-break-inside:avoid;}
}
</style>
</head>
<body>
<div class="topbar">
<div class="inner">
<div class="brand">
<div class="mark" aria-hidden="true"></div>
<div>
<b>Harmonious Wellness</b>
<div style="font-size:12px; color:#cbd5e1; font-weight:700;">Protocol Report (Mockup)</div>
</div>
</div>
<div class="actions">
<button id="pdfBtn" aria-pressed="false">📄 PDF Mode</button>
<button class="primary" onclick="window.print()">🖨️ Print / Save</button>
</div>
</div>
</div>
<main class="wrap">
<article class="paper" id="report"></article>
</main>
<script>
/*
PROTOCOL REPORT TEMPLATE
- Uses data from PROTOCOL (passed via reportbuilder)
- Resolves client, recommendations, and conditions via $J.get()
- Lightweight styling + small JS to avoid performance issues
- Web mode: expandable long descriptions
- PDF/Print: single-column, expanded
*/
// ---------------------------
// DATA TRANSFORMATION
// ---------------------------
const protocol = typeof PROTOCOL !== 'undefined' ? PROTOCOL : null;
function getRef(refId) {
if (!refId) return null;
try {
if (typeof $J !== 'undefined' && $J.get) {
return $J.get(refId);
}
} catch(e) {
console.error('Error getting reference:', refId, e);
}
return null;
}
function extractText(rendering) {
if (!rendering) return '';
if (typeof rendering === 'string') {
return rendering.replace(/<[^>]*>/g, '').trim();
}
return String(rendering);
}
function buildData() {
const data = {
protocol_name: '',
protocol_headline: '',
name: '',
email: '',
phone_number: '',
generated_on: '',
protocol_conditions: [],
main_concerns: [],
top_outcomes: [],
daily_diet_breakfast_lunch_dinner_snacks: '',
urine_ph: '',
urine_status: '',
sleep_quality: '',
daily_energy_level: '',
current_supplements: '',
current_medications: '',
client_symptoms: '',
phase_1_headline: '',
phase_1_description: '',
phase_1_recommendations: [],
phase_2_headline: '',
phase_2_description: '',
phase_2_recommendations: [],
phase_3_headline: '',
phase_3_description: '',
phase_3_recommendations: [],
phase_4_headline: '',
phase_4_description: '',
phase_4_recommendations: [],
phase_5_headline: '',
phase_5_description: '',
phase_5_recommendations: [],
blood_pressure_analysis_headline: '',
urine_analysis_headline: '',
condition_summaries: []
};
if (!protocol) {
console.warn('No protocol data available');
return data;
}
const client = protocol.client ? getRef(protocol.client) : null;
data.protocol_name = protocol.name || '';
data.protocol_headline = protocol.info || '';
if (client) {
data.name = client.name || '';
data.email = client.email || '';
data.phone_number = client.phone || '';
if (client.main_concerns) {
const concernsText = extractText(client.main_concerns);
data.main_concerns = concernsText ? concernsText.split(/\n|•|·|-/).map(s => s.trim()).filter(Boolean) : [];
}
if (client.top_outcomes) {
const outcomesText = extractText(client.top_outcomes);
data.top_outcomes = outcomesText ? outcomesText.split(/\n|•|·|-/).map(s => s.trim()).filter(Boolean) : [];
}
if (client.client_symptoms) {
data.client_symptoms = extractText(client.client_symptoms);
}
}
if (protocol.joeUpdated || protocol.created) {
const date = new Date(protocol.joeUpdated || protocol.created);
data.generated_on = date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
if (protocol.conditions && Array.isArray(protocol.conditions)) {
data.protocol_conditions = protocol.conditions.map(function(condId) {
const cond = getRef(condId);
return cond ? cond.name : null;
}).filter(Boolean);
}
if (protocol.urine_analysis_ph) {
data.urine_ph = String(protocol.urine_analysis_ph);
}
if (protocol.urine_status) {
data.urine_status = protocol.urine_status;
}
if (protocol.blood_pressure_analysis_headline) {
data.blood_pressure_analysis_headline = protocol.blood_pressure_analysis_headline;
}
if (protocol.urine_analysis_headline) {
data.urine_analysis_headline = protocol.urine_analysis_headline;
}
for (let i = 1; i <= 5; i++) {
const phaseHeadline = protocol[`phase_${i}_headline`] || '';
const phaseDesc = protocol[`phase_${i}_description`] ? extractText(protocol[`phase_${i}_description`]) : '';
const phaseRecIds = protocol[`phase_${i}_recommendations`] || [];
data[`phase_${i}_headline`] = phaseHeadline;
data[`phase_${i}_description`] = phaseDesc;
data[`phase_${i}_recommendations`] = phaseRecIds.map(function(recId) {
const rec = getRef(recId);
if (!rec) return null;
return {
recommendation_name: rec.name || '',
short_description: rec.info || '',
long_description: extractText(rec.description) || '',
standard_dosage: rec.standard_dosage || ''
};
}).filter(Boolean);
}
if (protocol.conditions && Array.isArray(protocol.conditions)) {
data.condition_summaries = protocol.conditions.map(function(condId) {
const cond = getRef(condId);
if (!cond) return null;
return {
protocol_conditions: cond.name || '',
long_description: extractText(cond.long_description || cond.description || '') || ''
};
}).filter(Boolean);
}
return data;
}
const data = buildData();
// ---------------------------
// Render helpers
// ---------------------------
const el = (tag, attrs={}, children=[]) => {
const n = document.createElement(tag);
Object.entries(attrs).forEach(([k,v]) => {
if(k === 'class') n.className = v;
else n.setAttribute(k, v);
});
(Array.isArray(children) ? children : [children]).forEach(c => {
if(c === null || c === undefined) return;
if(typeof c === 'string') n.appendChild(document.createTextNode(c));
else n.appendChild(c);
});
return n;
};
const pill = (label, value) => el('span', {class:'pill'}, [el('small', {}, label), ' ' + value]);
function recCard(rec){
const more = el('div', {class:'more'}, [el('div', {class:'muted'}, rec.long_description || '')]);
const toggle = el('button', {class:'toggle', type:'button', 'data-toggle':'more', 'aria-expanded':'false'}, '+');
return el('div', {class:'rec'}, [
el('div', {class:'rec-top'}, [
el('div', {}, [
el('div', {class:'rec-name'}, rec.recommendation_name),
el('div', {class:'muted', style:'margin-top:6px'}, [rec.short_description, ' ', toggle])
]),
el('div', {class:'dose'}, rec.standard_dosage || '')
]),
more
]);
}
function phase(n){
const headline = data[`phase_${n}_headline`];
const desc = data[`phase_${n}_description`];
const recs = data[`phase_${n}_recommendations`] || [];
return el('div', {class:'phase'}, [
el('div', {class:'head'}, [
el('div', {}, [
el('b', {}, headline),
el('p', {class:'muted'}, desc)
])
]),
el('div', {class:'recs'}, recs.map(recCard))
]);
}
// ---------------------------
// Render page (with DOM ready check)
// ---------------------------
function initReport() {
const report = document.getElementById('report');
if (!report) {
console.error('Report element not found');
return;
}
report.appendChild(el('section', {class:'hero'}, [
el('h1', {}, data.protocol_name),
el('div', {class:'sub'}, data.protocol_headline),
el('div', {class:'meta'}, [
pill('Client', data.name),
pill('Generated', data.generated_on),
pill('Email', data.email),
pill('Phone', data.phone_number)
])
]));
// Intentions and Goals
report.appendChild(el('section', {class:'section'}, [
el('h2', {}, 'Intentions and Goals'),
el('div', {class:'grid two'}, [
el('div', {class:'card'}, [
el('h3', {}, 'Conditions of focus'),
el('div', {class:'muted'}, data.protocol_conditions.join(', ')),
el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped System Field: protocol_conditions')
]),
el('div', {class:'card'}, [
el('h3', {}, 'Main Glands, Organs, or Systems Involved'),
el('div', {class:'muted'}, data.client_symptoms),
el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped System Field: client_symptoms')
])
]),
el('div', {class:'grid two', style:'margin-top:10px'}, [
el('div', {class:'card'}, [
el('h3', {}, 'Top Client Concerns'),
el('ul', {}, data.main_concerns.map(x => el('li', {}, x))),
el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped System Field: main_concerns')
]),
el('div', {class:'card'}, [
el('h3', {}, 'Other Key Notes from Consult'),
el('ul', {}, data.top_outcomes.map(x => el('li', {}, x))),
el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped System Field: top_outcomes')
])
]),
el('div', {class:'grid two', style:'margin-top:10px'}, [
el('div', {class:'card'}, [
el('h3', {}, 'Current Diet'),
el('div', {class:'muted'}, data.daily_diet_breakfast_lunch_dinner_snacks || 'Not specified'),
el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped System Field: daily_diet_breakfast_lunch_dinner_snacks')
]),
el('div', {class:'card'}, [
el('h3', {}, 'Kidney Filtration / Sleep / Energy'),
el('div', {class:'muted'}, `urine_ph: ${data.urine_ph || 'N/A'} · urine_status: ${data.urine_status || 'N/A'}`),
el('div', {class:'muted'}, `sleep_quality: ${data.sleep_quality || 'N/A'} · daily_energy_level: ${data.daily_energy_level || 'N/A'}`),
el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped Fields: urine_ph, urine_status, sleep_quality, daily_energy_level')
])
]),
el('div', {class:'grid two', style:'margin-top:10px'}, [
el('div', {class:'card'}, [
el('h3', {}, 'Current Supplements'),
el('div', {class:'muted'}, data.current_supplements || 'Not specified'),
el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped System Field: current_supplements')
]),
el('div', {class:'card'}, [
el('h3', {}, 'Current Medications'),
el('div', {class:'muted'}, data.current_medications || 'Not specified'),
el('div', {class:'muted', style:'font-size:12px; margin-top:8px'}, 'Mapped System Field: current_medications')
])
])
]));
// Phases & Recommendations
report.appendChild(el('section', {class:'section'}, [
el('h2', {}, 'Protocol Phases & Recommendations'),
phase(1), phase(2), phase(3), phase(4), phase(5)
]));
// Education & Resources
report.appendChild(el('section', {class:'section'}, [
el('h2', {}, 'Education & Resources'),
el('div', {class:'card'}, [
el('h3', {}, 'Understanding Your Body Signals'),
el('div', {class:'muted'}, `blood_pressure_analysis_headline: ${data.blood_pressure_analysis_headline || 'N/A'}`),
el('div', {class:'muted', style:'margin-top:8px'}, `urine_analysis_headline: ${data.urine_analysis_headline || 'N/A'}`)
]),
el('div', {class:'card', style:'margin-top:10px'}, [
el('h3', {}, "Key Conditions We’re Supporting"),
...data.condition_summaries.map(c => el('div', {style:'margin-top:10px'}, [
el('div', {style:'font-weight:900; color:var(--plum)'}, c.protocol_conditions),
el('div', {class:'muted'}, c.long_description)
]))
]),
el('div', {class:'card', style:'margin-top:10px'}, [
el('h3', {}, 'Products Referenced in 20-Week Protocol'),
el('div', {class:'muted'}, 'List and links for products referenced in this protocol will be emailed separately.')
])
]));
// Closing Note (static rendered content)
report.appendChild(el('section', {class:'section'}, [
el('h2', {}, 'Closing Note'),
el('div', {class:'muted'},
`${data.name ? data.name.split(' ')[0] : 'Client'}, This is a ton of information and oftentimes it takes some digestion and processing. If you would like to book a follow up appointment or just hop on the phone to discuss any of this please let me know. I am here to support you! I also make tailor made tinctures and teas so feel free to let me know if that is of interest. Deep blessings on the next step in your health journey! I am available by email or text if you have questions along the way. Please never hesitate to reach out! I would love to at least connect back in 3 months to hear how things are going and recommend adjustments/modifications as needed. You can book your follow-up appointment online here. Select either a Short Follow up or Long Follow up. There is a small fee for these follow ups. However, accessibility is a value of mine and I will never turn anyone away for lack of funds. Please let me know and I will share my sliding scale range. Thank you ${data.name || 'you'} for trusting and allowing me to support you. The work works. Keep the faith. Live inspired. Blessings on your wellness journey and enhanced vitality! ~ Kelly Shay`
)
]));
// ---------------------------
// Interactions
// ---------------------------
const pdfBtn = document.getElementById('pdfBtn');
if (pdfBtn) {
pdfBtn.addEventListener('click', () => {
const enabled = !document.body.classList.contains('pdf-mode');
document.body.classList.toggle('pdf-mode', enabled);
pdfBtn.setAttribute('aria-pressed', String(enabled));
});
}
document.addEventListener('click', (e) => {
const t = e.target.closest('[data-toggle="more"]');
if(!t) return;
if(document.body.classList.contains('pdf-mode')) return;
const rec = t.closest('.rec');
if (!rec) return;
const more = rec.querySelector('.more');
if (!more) return;
const expanded = t.getAttribute('aria-expanded') === 'true';
t.setAttribute('aria-expanded', String(!expanded));
more.style.display = expanded ? 'none' : 'block';
});
}
// Wait for DOM to be ready before initializing
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initReport);
} else {
// DOM already ready, but wait a tick to ensure elements exist
setTimeout(initReport, 0);
}
</script>
</body>
</html>