DEV Community

Cover image for React Coding Challenge : Stepper Form V1
ZeeshanAli-0704
ZeeshanAli-0704

Posted on

React Coding Challenge : Stepper Form V1

import { useState, Fragment } from "react";
import "./styles.css";

const Stepper = ({ steps, onFinish }) => {
  const [active, setActive] = useState(0);
  const total = steps.length;
  const isLast = active === total - 1;

  const goNext = () => {
    const current = steps[active];
    if (current?.validate && !current.validate()) return;
    if (!isLast) setActive((s) => s + 1);
    else onFinish?.();
  };

  const goBack = () => {
    if (active > 0) setActive((s) => s - 1);
  };

  return (
    <div className="stepperLayout">
      <div className="stepperRail">
        {steps.map((_, i) => {
          const state =
            i < active ? "completed" : i === active ? "active" : "upcoming";
          return (
            <Fragment key={i}>
              <div className={`stepperDot ${state}`} />
              {i !== steps.length - 1 && (
                <div
                  className={`stepperLine ${i < active ? "completed" : ""}`}
                />
              )}
            </Fragment>
          );
        })}
      </div>

      <div className="stepperPanel">
        <div className="stepperContent">{steps[active].content}</div>

        <div className="stepperActions">
          <button
            type="button"
            className="btn"
            onClick={goBack}
            disabled={active === 0}
          >
            Back
          </button>
          <button type="button" className="btn primary" onClick={goNext}>
            {isLast ? "Submit" : "Next"}
          </button>
        </div>
      </div>
    </div>
  );
};

const FormA = ({ name, setName, age, setAge }) => (
  <form className="form" onSubmit={(e) => e.preventDefault()}>
    <div className="formRow">
      <label htmlFor="name" className="formLabel">
        Name
      </label>
      <input
        id="name"
        className="formInput"
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Enter your name"
      />
    </div>
    <div className="formRow">
      <label htmlFor="age" className="formLabel">
        Age
      </label>
      <input
        id="age"
        className="formInput"
        type="number"
        value={age}
        onChange={(e) => setAge(e.target.value)}
        placeholder="Enter your age"
      />
    </div>
  </form>
);

const FormB = ({ email, setEmail }) => (
  <form className="form" onSubmit={(e) => e.preventDefault()}>
    <div className="formRow">
      <label htmlFor="email" className="formLabel">
        Email
      </label>
      <input
        id="email"
        className="formInput"
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="you@company.com"
      />
    </div>
  </form>
);

const FormC = ({ notes, setNotes }) => (
  <form className="form" onSubmit={(e) => e.preventDefault()}>
    <div className="formRow">
      <label htmlFor="notes" className="formLabel">
        Notes
      </label>
      <input
        id="notes"
        className="formInput"
        type="text"
        value={notes}
        onChange={(e) => setNotes(e.target.value)}
        placeholder="Optional"
      />
    </div>
  </form>
);

export default function App() {
  const [name, setName] = useState("");
  const [age, setAge] = useState("");
  const [email, setEmail] = useState("");
  const [notes, setNotes] = useState("");

  const steps = [
    {
      label: "Form 1",
      content: (
        <FormA name={name} setName={setName} age={age} setAge={setAge} />
      ),
      validate: () => name.trim().length > 0 && Number(age) > 0,
    },
    {
      label: "Form 2",
      content: <FormB email={email} setEmail={setEmail} />,
      validate: () => /\S+@\S+\.\S+/.test(email),
    },
    {
      label: "Form 3",
      content: <FormC notes={notes} setNotes={setNotes} />,
    },
  ];

  const handleFinish = () => {
    alert(`Submitted: ${JSON.stringify({ name, age, email, notes }, null, 2)}`);
  };

  return (
    <div className="App">
      <h2>Vertical Stepper Form</h2>
      <Stepper steps={steps} onFinish={handleFinish} />
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode
.App {
  font-family: sans-serif;
  text-align: center;
  padding: 24px;
}

.stepperLayout {
  display: grid;
  grid-template-columns: 48px 1fr;
  gap: 16px;
  align-items: start;
}

.stepperRail {
  display: flex;
  flex-direction: column;
  align-items: center;
  position: relative;
  margin-top: 8px;
}

.stepperDot {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid #d0d5dd;
  background: #fff;
}

.stepperDot.active {
  border-color: #276ef1;
  background: #276ef1;
}

.stepperDot.completed {
  border-color: #276ef1;
  background: #276ef1;
  opacity: 0.8;
}

.stepperLine {
  width: 2px;
  height: 40px;
  background: #e6e8eb;
}

.stepperLine.completed {
  background: #276ef1;
}

.stepperPanel {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.stepperContent {
  flex: 1;
}

.stepperActions {
  display: flex;
  gap: 12px;
  justify-content: flex-end;
}

.btn {
  padding: 8px 14px;
  border-radius: 8px;
  border: 1px solid #c5c7d0;
  background: #fff;
  cursor: pointer;
}

.btn.primary {
  border-color: #276ef1;
  background: #276ef1;
  color: #fff;
}

/* Form styles */
.form {
  background: #ffffff;
  border-radius: 8px;
  padding: 16px;
}
.formRow {
  display: flex;
  flex-direction: row;
  gap: 6px;
  margin-bottom: 12px;
  align-items: center;
  width: 100%;
}
.formLabel {
  font-size: 14px;
  color: #333;
  width: 20%;
}
.formInput {
  padding: 8px 10px;
  border: 1px solid #c5c7d0;
  border-radius: 6px;
  font-size: 14px;
  outline: none;
  background: #fff;
  width: 100%;
}
.formInput:focus {
  border-color: #276ef1;
  box-shadow: 0 0 0 3px rgba(39, 110, 241, 0.15);
}
.formInput::placeholder {
  color: #9aa0a6;
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)