diff --git a/15-multi-step-form/build.sbt b/15-multi-step-form/build.sbt index ebb5ed3..2140cbb 100644 --- a/15-multi-step-form/build.sbt +++ b/15-multi-step-form/build.sbt @@ -11,6 +11,7 @@ lazy val multiStepForm = (project in file(".")) libraryDependencies ++= Seq( "com.lihaoyi" %% "cask" % "0.9.1", "com.lihaoyi" %% "mainargs" % "0.5.0", - "org.thymeleaf" % "thymeleaf" % "3.1.1.RELEASE" + "org.thymeleaf" % "thymeleaf" % "3.1.1.RELEASE", + "com.googlecode.libphonenumber" % "libphonenumber" % "8.13.16" ) ) diff --git a/15-multi-step-form/src/main/resources/public/out.css b/15-multi-step-form/src/main/resources/public/out.css index b8895df..52e2faf 100644 --- a/15-multi-step-form/src/main/resources/public/out.css +++ b/15-multi-step-form/src/main/resources/public/out.css @@ -572,10 +572,6 @@ video { grid-row: span 2 / span 2; } -.row-span-full { - grid-row: 1 / -1; -} - .row-start-1 { grid-row-start: 1; } @@ -655,6 +651,10 @@ video { display: inline-grid; } +.contents { + display: contents; +} + .hidden { display: none; } @@ -777,14 +777,6 @@ video { grid-template-columns: repeat(4, auto); } -.grid-rows-5 { - grid-template-rows: repeat(5, minmax(0, 1fr)); -} - -.grid-rows-3 { - grid-template-rows: repeat(3, minmax(0, 1fr)); -} - .flex-row { flex-direction: row; } @@ -879,6 +871,11 @@ video { border-color: rgb(255 255 255 / var(--tw-border-opacity)); } +.border-strawberry-red { + --tw-border-opacity: 1; + border-color: hsl(354 84% 57% / var(--tw-border-opacity)); +} + .\!bg-light-blue { --tw-bg-opacity: 1 !important; background-color: hsl(206 94% 87% / var(--tw-bg-opacity)) !important; @@ -1063,6 +1060,11 @@ video { color: rgb(255 255 255 / var(--tw-text-opacity)); } +.text-strawberry-red { + --tw-text-opacity: 1; + color: hsl(354 84% 57% / var(--tw-text-opacity)); +} + .underline { text-decoration-line: underline; } diff --git a/15-multi-step-form/src/main/resources/templates/index.html b/15-multi-step-form/src/main/resources/templates/index.html index 417fba0..cbeaeaa 100644 --- a/15-multi-step-form/src/main/resources/templates/index.html +++ b/15-multi-step-form/src/main/resources/templates/index.html @@ -33,13 +33,14 @@
-
+
- loading... + loading...
diff --git a/15-multi-step-form/src/main/resources/templates/step1.html b/15-multi-step-form/src/main/resources/templates/step1.html index 617c27e..d7a569c 100644 --- a/15-multi-step-form/src/main/resources/templates/step1.html +++ b/15-multi-step-form/src/main/resources/templates/step1.html @@ -126,42 +126,81 @@

Please provide your name, email address, and phone number.

- + + + +
+ + +
+ +
- - - - - +
+ + +
Please enter valid phone number
+ +
+
diff --git a/15-multi-step-form/src/main/scala/multistepform/Routes.scala b/15-multi-step-form/src/main/scala/multistepform/Routes.scala index 367bb2d..5f777ba 100644 --- a/15-multi-step-form/src/main/scala/multistepform/Routes.scala +++ b/15-multi-step-form/src/main/scala/multistepform/Routes.scala @@ -8,7 +8,11 @@ import java.util.UUID import scala.jdk.CollectionConverters._ import multistepform.Models.Answers import scala.annotation.internal.requiresCapability +import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber.CountryCodeSource +import com.google.i18n.phonenumbers.PhoneNumberUtil + import java.net.URLDecoder +import scala.util.Try case class Routes()(implicit cc: castor.Context, log: cask.Logger) extends cask.Routes { @@ -103,9 +107,10 @@ case class Routes()(implicit cc: castor.Context, log: cask.Logger) // and that is not nice val userAnswers = Sessions.sessionReplies.getOrElse(id, Answers(id)) - val submittedData = URLDecoder.decode(request.text() , "UTF-8") + val submittedData = URLDecoder.decode(request.text(), "UTF-8") - val updatedAnswers = userAnswers.updateStep(stepNum, submittedData, nextStep) + val updatedAnswers = + userAnswers.updateStep(stepNum, submittedData, nextStep) Sessions.sessionReplies.update(id, updatedAnswers) @@ -124,25 +129,75 @@ case class Routes()(implicit cc: castor.Context, log: cask.Logger) ) } - /** - * This endpoint re-renders 'plan type inputs' - * so that togglng monthly\yearly could redraw their html - */ + @cask.post("/step1/phonenumber") + def validateStep1PhoneNumber(request: cask.Request) = { + val submittedData = URLDecoder.decode(request.text(), "UTF-8") + println( + s"getting data ${request.data} or ${request.text()} or $submittedData" + ) + val fieldValues = submittedData + .split("&") + .filterNot(_.isEmpty()) + .map { field => + println(s"looking at field $field") + val (name, value) = field.split("=").toList match { + case List(name, value) => name -> value + case name :: tail => name -> tail.headOption.getOrElse("") + case Nil => ??? + } + name -> value + } + .toMap + + val name = fieldValues.getOrElse("name", "") + val email = fieldValues.getOrElse("email", "") + val phone = fieldValues.getOrElse("phone", "") + + println(s"after parsing name=$name | email=$email | phone=$phone") + + val phoneUtils = PhoneNumberUtil.getInstance() + val phoneNum = Try( + phoneUtils.parse(phone, CountryCodeSource.UNSPECIFIED.name()) + ).toOption + val isPhoneValid = phoneNum.map(phoneUtils.isValidNumber(_)).getOrElse(false) + + val error = if (isPhoneValid) "" else "Please input valid phone number" + + val context = new Context() + context.setVariable("value", phone) + context.setVariable("error", error) + val inputDiv = + templateEngine.process("step1", Set("email-input").asJava, context) + + cask.Response( + inputDiv, + headers = Seq("Content-Type" -> "text/html;charset=UTF-8") + ) + } + + /** This endpoint re-renders 'plan type inputs' so that togglng monthly\yearly + * could redraw their html + */ @cask.post("/step2/planTypeInputs") def getPlanTypeInputs(sessionId: cask.Cookie, request: cask.Request) = { val id = sessionId.value - val submittedData = URLDecoder.decode(request.text() , "UTF-8") + val submittedData = URLDecoder.decode(request.text(), "UTF-8") println(s"requesting plan type inputs for $id and $request") Sessions.sessionReplies.get(id) match { case None => - cask.Response("Can't find answers for your session, please reload the page", 404) + cask.Response( + "Can't find answers for your session, please reload the page", + 404 + ) case Some(answers) => { // here changing yearly/monthly part of state based on passed checkbox value // and selected plan // only for purposes of rendering the fragment // not persisting, unless next or previous buttons are pressed - val withYearlyParamSetAndSelectedPlan = answers.step2.fromFormData(submittedData) - val updatedState = answers.copy(step2 = withYearlyParamSetAndSelectedPlan) + val withYearlyParamSetAndSelectedPlan = + answers.step2.fromFormData(submittedData) + val updatedState = + answers.copy(step2 = withYearlyParamSetAndSelectedPlan) val formData = Models.FormData(updatedState) val context = new Context() context.setVariable(formDataContextVarName, formData)