json-object-editor
Version:
JOE the Json Object Editor | Platform Edition
571 lines (544 loc) • 21.6 kB
JavaScript
import React, { useMemo, useState } from "react";
// Harmonious Wellness Questionnaire – interactive mockup
// - Multi-step sections
// - Conditional visibility
// - Basic required validation
// - Review + JSON export
const FORM = {
formName: "Harmonious Wellness Health Questionnaire",
version: "2026-01-04",
sections: [
{
id: "demographics",
title: "Demographics",
description: "Basic identifying and contact details.",
fields: [
{ id: "first_name", label: "First Name", type: "text", required: true, placeholder: "First name" },
{ id: "last_name", label: "Last Name", type: "text", required: true, placeholder: "Last name" },
{
id: "gender",
label: "Gender",
type: "select",
required: true,
options: [
{ value: "female", label: "Female" },
{ value: "male", label: "Male" },
{ value: "nonbinary", label: "Non-binary" },
{ value: "prefer_not_to_say", label: "Prefer not to say" }
]
},
{ id: "age", label: "Age", type: "number", required: true, min: 0, max: 120 },
{ id: "height", label: "Height", type: "text", required: true, placeholder: "e.g., 5'8\" or 173 cm" },
{ id: "weight", label: "Weight", type: "text", required: true, placeholder: "e.g., 165 lb or 75 kg" },
{ id: "email", label: "Email Address", type: "email", required: true, placeholder: "name@email.com" },
{ id: "location_time_zone", label: "Location / Time Zone", type: "text", required: true, placeholder: "City, State and Time Zone" },
{ id: "phone_number", label: "Phone Number", type: "phone", required: true, placeholder: "(555) 555-5555" },
{
id: "preferred_communication_method",
label: "Preferred Communication Method",
type: "select",
required: true,
options: [
{ value: "text", label: "Text" },
{ value: "email", label: "Email" },
{ value: "phone", label: "Phone Call" }
]
}
]
},
{
id: "primary_concerns_history",
title: "Primary Concerns and Medical History",
description: "Your main concerns and key medical background.",
fields: [
{
id: "main_health_concerns",
label: "Main Health Concerns you'd like support on",
type: "textarea",
required: true,
placeholder: "Briefly describe your top concerns."
},
{
id: "diagnosed_medical_conditions",
label: "Diagnosed Medical Conditions",
type: "textarea",
required: true,
placeholder: "List diagnoses or type “None”."
},
{
id: "current_medications",
label: "Current Medications",
type: "text",
required: true,
placeholder: "List medications or type “None”."
},
{
id: "current_supplements",
label: "Current Supplements",
type: "text",
required: true,
placeholder: "List supplements or type “None”."
},
{
id: "past_major_surgeries",
label: "Past Major Surgeries",
type: "text",
required: true,
placeholder: "List surgeries or type “N/A”."
},
{
id: "pregnant_or_breastfeeding",
label: "Pregnant or Breastfeeding",
type: "boolean",
required: true,
visibility: { whenAll: [{ field: "gender", op: "eq", value: "female" }] }
},
{ id: "gallbladder_removed", label: "Gallbladder Removed", type: "boolean", required: true },
{
id: "history_of_heart_issues",
label: "History of Heart Issues (Arrhythmia, Stent, Pacemaker, etc.)",
type: "text",
required: true,
placeholder: "Describe or type “N/A”."
},
{
id: "cancer_history_explain",
label: "Cancer History (Please explain)",
type: "text",
required: true,
visibility: {
whenAny: [
{ field: "diagnosed_medical_conditions", op: "contains", value: "cancer" },
{ field: "diagnosed_medical_conditions", op: "contains", value: "Cancer" }
]
},
placeholder: "Type and location, or “N/A”."
},
{
id: "diabetes_type",
label: "Diabetes Type",
type: "select",
required: true,
options: [
{ value: "none", label: "None" },
{ value: "type_1", label: "Type 1" },
{ value: "type_2", label: "Type 2" },
{ value: "unsure", label: "Unsure" }
]
},
{
id: "known_allergies_food_environmental",
label: "Known Allergies (Food/Environmental)",
type: "text",
required: true,
placeholder: "List allergies or type “N/A”."
}
]
},
{
id: "gi_digestion",
title: "Digestion and Elimination",
description: "Digestive function, reactions, and stool patterns.",
fields: [
{ id: "overall_digestion", label: "Overall Digestion", type: "scale", required: true, min: 0, max: 5 },
{ id: "bloating_after_meals", label: "Bloating After Meals", type: "scale", required: true, min: 0, max: 5 },
{ id: "reaction_to_fats", label: "Reaction to Fats", type: "scale", required: true, min: 0, max: 5 },
{ id: "gas_frequency", label: "Gas Frequency", type: "scale", required: false, min: 0, max: 5 },
{ id: "constipation_tendency", label: "Constipation Tendency", type: "scale", required: true, min: 0, max: 5 },
{ id: "diarrhea_tendency", label: "Diarrhea Tendency", type: "scale", required: true, min: 0, max: 5 },
{
id: "stool_form_bristol",
label: "Stool Form (Bristol Scale)",
type: "select",
required: true,
options: [
{ value: "1", label: "Type 1 (hard lumps)" },
{ value: "2", label: "Type 2 (lumpy sausage)" },
{ value: "3", label: "Type 3 (cracked sausage)" },
{ value: "4", label: "Type 4 (smooth sausage)" },
{ value: "5", label: "Type 5 (soft blobs)" },
{ value: "6", label: "Type 6 (mushy)" },
{ value: "7", label: "Type 7 (watery)" }
]
},
{
id: "stool_frequency",
label: "Stool Frequency",
type: "select",
required: true,
options: [
{ value: "0_1", label: "0–1 per day" },
{ value: "1_2", label: "1–2 per day" },
{ value: "2_3", label: "2–3 per day" },
{ value: "3_plus", label: "3+ per day" }
]
},
{ id: "food_sensitivities", label: "Food Sensitivities", type: "text", required: false, placeholder: "List foods or type “N/A”." },
{ id: "acid_reflux_heartburn", label: "Acid Reflux / Heartburn", type: "scale", required: true, min: 0, max: 5 }
]
},
{
id: "hormones_repro",
title: "Hormones and Reproductive Health",
description: "Hormonal symptoms and sex-specific health signals.",
fields: [
{ id: "hormonal_imbalance_symptoms", label: "Hormonal Imbalance Symptoms", type: "scale", required: true, min: 0, max: 5 },
{ id: "temperature_sensitivity", label: "Temperature Sensitivity", type: "scale", required: true, min: 0, max: 5 },
{ id: "hot_flashes_night_sweats", label: "Hot Flashes / Night Sweats", type: "boolean", required: true },
{
id: "libido_level",
label: "Libido Level",
type: "select",
required: true,
options: [
{ value: "low", label: "Low" },
{ value: "moderate", label: "Moderate" },
{ value: "high", label: "High" }
]
},
{
id: "cycle_regularity",
label: "Cycle Regularity",
type: "boolean",
required: true,
visibility: { whenAll: [{ field: "gender", op: "eq", value: "female" }] }
},
{
id: "pms_severity",
label: "PMS Severity",
type: "scale",
required: true,
min: 0,
max: 5,
visibility: { whenAll: [{ field: "gender", op: "eq", value: "female" }] }
},
{
id: "menstrual_pain",
label: "Menstrual Pain",
type: "scale",
required: true,
min: 0,
max: 5,
visibility: { whenAll: [{ field: "gender", op: "eq", value: "female" }] }
},
{
id: "prostate_symptoms",
label: "Prostate Symptoms",
type: "boolean",
required: true,
visibility: { whenAll: [{ field: "gender", op: "eq", value: "male" }] }
},
{
id: "erectile_function_concerns",
label: "Erectile Function Concerns",
type: "boolean",
required: true,
visibility: { whenAll: [{ field: "gender", op: "eq", value: "male" }] }
}
]
},
{
id: "vitals_diet_family",
title: "Vitals, Diet, and Family History",
description: "Key anchors grouped and easy to reference.",
fields: [
{ id: "blood_pressure_right", label: "Blood Pressure Right", type: "text", required: true, placeholder: "e.g., 120/80 or type “N/A”" },
{ id: "blood_pressure_left", label: "Blood Pressure Left", type: "text", required: true, placeholder: "e.g., 120/80 or type “N/A”" },
{ id: "urine_ph", label: "Urine pH", type: "number", required: true, min: 0, max: 14, step: 0.1, placeholder: "e.g., 6.5" },
{
id: "daily_diet_breakfast_lunch_dinner_snacks",
label: "What does your current daily diet consist of? (Breakfast/Lunch/Dinner/Snacks)",
type: "textarea",
required: true,
placeholder: "List typical breakfast, lunch, dinner, and snacks."
},
{
id: "family_history_all",
label: "Please list all known health concerns for each family member (Mother/Father/Grandparents/Siblings). Leave blank if you aren’t sure.",
type: "textarea",
required: true,
placeholder: "Mother: ... Father: ... Grandparents: ... Siblings: ..."
}
]
}
]
};
function clsx(...xs) {
return xs.filter(Boolean).join(" ");
}
function evalCondition(cond, values) {
const v = values?.[cond.field];
switch (cond.op) {
case "eq":
return v === cond.value;
case "neq":
return v !== cond.value;
case "contains":
return typeof v === "string" ? v.toLowerCase().includes(String(cond.value).toLowerCase()) : false;
case "gt":
return Number(v) > Number(cond.value);
case "gte":
return Number(v) >= Number(cond.value);
case "lt":
return Number(v) < Number(cond.value);
case "lte":
return Number(v) <= Number(cond.value);
case "truthy":
return Boolean(v);
default:
return false;
}
}
function isVisible(field, values) {
const vis = field.visibility;
if (!vis) return true;
if (vis.whenAll) return vis.whenAll.every((c) => evalCondition(c, values));
if (vis.whenAny) return vis.whenAny.some((c) => evalCondition(c, values));
return true;
}
function Field({ field, value, onChange, error }) {
const common = {
id: field.id,
name: field.id,
className:
"w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm shadow-sm focus:border-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-200",
value: value ?? "",
onChange: (e) => onChange(field.id, e.target.value)
};
return (
<div className="space-y-1">
<div className="flex items-start justify-between gap-3">
<label htmlFor={field.id} className="text-sm font-medium text-slate-900">
{field.label}
{field.required ? <span className="ml-1 text-rose-600">*</span> : null}
</label>
</div>
{field.type === "textarea" ? (
<textarea {...common} rows={4} placeholder={field.placeholder || ""} />
) : field.type === "select" ? (
<select
{...common}
value={value ?? ""}
onChange={(e) => onChange(field.id, e.target.value)}
>
<option value="">Select…</option>
{field.options?.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
) : field.type === "boolean" ? (
<div className="flex items-center gap-3">
<label className="inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm shadow-sm">
<input
type="radio"
name={field.id}
checked={value === true}
onChange={() => onChange(field.id, true)}
/>
Yes
</label>
<label className="inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm shadow-sm">
<input
type="radio"
name={field.id}
checked={value === false}
onChange={() => onChange(field.id, false)}
/>
No
</label>
</div>
) : field.type === "scale" ? (
<div className="space-y-2">
<input
type="range"
min={field.min ?? 0}
max={field.max ?? 5}
step={1}
value={value ?? 0}
onChange={(e) => onChange(field.id, Number(e.target.value))}
className="w-full"
/>
<div className="flex items-center justify-between text-xs text-slate-500">
<span>{field.minLabel ?? field.min ?? 0}</span>
<span className="rounded-lg bg-slate-100 px-2 py-1 text-slate-700">{String(value ?? 0)}</span>
<span>{field.maxLabel ?? field.max ?? 5}</span>
</div>
</div>
) : (
<input
{...common}
type={field.type === "number" ? "number" : field.type === "email" ? "email" : "text"}
min={field.min}
max={field.max}
step={field.step}
placeholder={field.placeholder || ""}
/>
)}
{error ? <p className="text-xs text-rose-600">{error}</p> : null}
</div>
);
}
function validateSection(section, values) {
const errors = {};
for (const f of section.fields) {
if (!isVisible(f, values)) continue;
if (!f.required) continue;
const v = values[f.id];
const empty = v === undefined || v === null || v === "";
if (f.type === "boolean") {
if (v !== true && v !== false) errors[f.id] = "Please select Yes or No.";
} else if (f.type === "scale") {
if (v === undefined || v === null) errors[f.id] = "Please choose a value.";
} else if (empty) {
errors[f.id] = "This field is required.";
}
}
return errors;
}
export default function App() {
const [step, setStep] = useState(0);
const [values, setValues] = useState({});
const [errors, setErrors] = useState({});
const [showReview, setShowReview] = useState(false);
const sections = FORM.sections;
const current = sections[step];
const visibleFields = useMemo(() => {
return current.fields.filter((f) => isVisible(f, values));
}, [current, values]);
function setValue(id, v) {
setValues((prev) => ({ ...prev, [id]: v }));
setErrors((prev) => {
if (!prev[id]) return prev;
const next = { ...prev };
delete next[id];
return next;
});
}
function next() {
const e = validateSection(current, values);
setErrors(e);
if (Object.keys(e).length) return;
if (step < sections.length - 1) {
setStep(step + 1);
window.scrollTo({ top: 0, behavior: "smooth" });
} else {
setShowReview(true);
window.scrollTo({ top: 0, behavior: "smooth" });
}
}
function back() {
if (showReview) {
setShowReview(false);
return;
}
setStep(Math.max(0, step - 1));
window.scrollTo({ top: 0, behavior: "smooth" });
}
return (
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white">
<div className="mx-auto max-w-3xl px-4 py-8">
<header className="mb-6">
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-semibold text-slate-900">{FORM.formName}</h1>
<p className="text-sm text-slate-600">
Version {FORM.version} • Interactive mockup with conditional logic
</p>
</div>
<div className="mt-4 h-2 w-full overflow-hidden rounded-full bg-slate-100">
<div
className="h-full rounded-full bg-slate-900 transition-all"
style={{ width: `${showReview ? 100 : ((step + 1) / sections.length) * 100}%` }}
/>
</div>
<div className="mt-2 flex items-center justify-between text-xs text-slate-500">
<span>
{showReview ? "Review" : `Section ${step + 1} of ${sections.length}`}
</span>
<span>{showReview ? "Ready" : current.title}</span>
</div>
</header>
<main className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
{!showReview ? (
<>
<div className="mb-4">
<h2 className="text-lg font-semibold text-slate-900">{current.title}</h2>
<p className="mt-1 text-sm text-slate-600">{current.description}</p>
</div>
<div className="grid gap-4">
{visibleFields.map((f) => (
<Field
key={f.id}
field={f}
value={values[f.id]}
onChange={setValue}
error={errors[f.id]}
/>
))}
</div>
<div className="mt-6 flex items-center justify-between">
<button
onClick={back}
disabled={step === 0}
className={clsx(
"rounded-xl px-4 py-2 text-sm font-medium",
step === 0
? "cursor-not-allowed bg-slate-100 text-slate-400"
: "bg-slate-100 text-slate-800 hover:bg-slate-200"
)}
>
Back
</button>
<button
onClick={next}
className="rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800"
>
{step === sections.length - 1 ? "Review" : "Next"}
</button>
</div>
<div className="mt-4 rounded-xl bg-slate-50 p-3 text-xs text-slate-600">
Tip: Set <strong>Gender</strong> to Female to reveal pregnancy and cycle questions, or Male to reveal prostate-related questions.
Add the word <strong>cancer</strong> in “Diagnosed Medical Conditions” to reveal the Cancer History field.
</div>
</>
) : (
<>
<div className="mb-4">
<h2 className="text-lg font-semibold text-slate-900">Review & Export</h2>
<p className="mt-1 text-sm text-slate-600">
This is the payload your system can ingest after submission.
</p>
</div>
<div className="rounded-xl border border-slate-200 bg-slate-50 p-3">
<pre className="max-h-[55vh] overflow-auto text-xs text-slate-800">
{JSON.stringify(values, null, 2)}
</pre>
</div>
<div className="mt-6 flex items-center justify-between">
<button
onClick={back}
className="rounded-xl bg-slate-100 px-4 py-2 text-sm font-medium text-slate-800 hover:bg-slate-200"
>
Back
</button>
<button
onClick={() => {
navigator.clipboard.writeText(JSON.stringify(values, null, 2));
alert("Copied JSON to clipboard.");
}}
className="rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800"
>
Copy JSON
</button>
</div>
</>
)}
</main>
<footer className="mt-6 text-xs text-slate-500">
Mockup for internal testing. Not production-hardened. Conditional logic and structure are designed to be reusable.
</footer>
</div>
</div>
);
}