UNPKG

json-object-editor

Version:

JOE the Json Object Editor | Platform Edition

571 lines (544 loc) 21.6 kB
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> ); }