package multistepform import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver import org.thymeleaf.TemplateEngine import org.thymeleaf.context.Context import cask.endpoints.ParamReader import java.util.UUID import scala.jdk.CollectionConverters._ import multistepform.Models.Answers import scala.annotation.internal.requiresCapability import java.net.URLDecoder case class Routes()(implicit cc: castor.Context, log: cask.Logger) extends cask.Routes { val templateResolver = new ClassLoaderTemplateResolver() templateResolver.setPrefix("templates/"); templateResolver.setSuffix(".html") templateResolver.setTemplateMode("HTML5") val templateEngine = new TemplateEngine() templateEngine.setTemplateResolver(templateResolver) val sessoinCookieName = "sessionId" /** This route works with and without 'sessionId' cookie present set's this * cookie if not present, and returns initial 'index.html' where the form is * not yet initialized, and will be requested for the session */ @cask.get("/") def getIndex(ctx: cask.Request) = { val sessionCookie = ctx.cookies.get(sessoinCookieName) lazy val newSessionCookies = sessionCookie match { case None => Seq( cask.Cookie( sessoinCookieName, UUID.randomUUID().toString(), path = "/" ) ) case Some(_) => Seq.empty // don't set new cookies } println(s"getting cookie $sessionCookie will set new? ${newSessionCookies}") val context = new Context() val indexPage = templateEngine.process("index", context) cask.Response( indexPage, headers = Seq("Content-Type" -> "text/html;charset=UTF-8"), cookies = newSessionCookies ) } @cask.get("/force-new-session") def forceNewSession() = { val newSessionCookie = cask.Cookie(sessoinCookieName, UUID.randomUUID().toString(), path = "/") println(s"setting new session ${newSessionCookie.value}") cask.Response( s"New session forced. Force new session", headers = Seq("Content-Type" -> "text/html;charset=UTF-8"), cookies = Seq(newSessionCookie) ) } val formDataContextVarName = "formData" /** This method only works when cookie 'sessionId' is present will get or init * Form State for the session, and return last unsubmitted form step fragment */ @cask.get("/get-form") def getForm(sessionId: cask.Cookie) = { val id = sessionId.value val state = Sessions.sessionReplies.getOrElse(id, Answers(id)) println(s"starting form for $state") val context = new Context() val formData = Models.FormData(state) context.setVariable(formDataContextVarName, formData) val formFragment = templateEngine.process( state.fragmentName, Set("formFragment").asJava, context ) cask.Response( formFragment, headers = Seq("Content-Type" -> "text/html;charset=UTF-8") ) } @cask.post("/submit-step/:stepNum/:nextStep") def submitStep( sessionId: cask.Cookie, stepNum: Int, nextStep: Int, request: cask.Request ) = { val id = sessionId.value println(s"got $request for $id. it's data is ${request.text()}") // note: this is nice at step #1 because not storing anything before first submission // but on followup steps, if data lost - new default object is created // and that is not nice val userAnswers = Sessions.sessionReplies.getOrElse(id, Answers(id)) val submittedData = URLDecoder.decode(request.text() , "UTF-8") val updatedAnswers = userAnswers.updateStep(stepNum, submittedData, nextStep) Sessions.sessionReplies.update(id, updatedAnswers) val context = new Context() val formData = Models.FormData(updatedAnswers) context.setVariable(formDataContextVarName, formData) val nextFormFragment = templateEngine.process( updatedAnswers.fragmentName, Set("formFragment").asJava, context ) println(s"the state now is $updatedAnswers") cask.Response( nextFormFragment, 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.get("/step2/planTypeInputs") def getPlanTypeInputs(sessionId: cask.Cookie, isPackageYearly: Option[String] = None) = { val id = sessionId.value val isPackageYearlyBool = isPackageYearly.contains("on") println(s"requesting plan type inputs for $id and $isPackageYearlyBool") Sessions.sessionReplies.get(id) match { case None => 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 // only for purposes of rendering the fragment // not persisting, unless next or previous buttons are pressed val withYearlyParamSet = answers.copy(step2 = answers.step2.copy(isYearly = isPackageYearlyBool)) val formData = Models.FormData(withYearlyParamSet) val context = new Context() context.setVariable(formDataContextVarName, formData) val planTypesInputsHtml = templateEngine.process( "step2", Set("planTypesInputs").asJava, context ) println(s"updating plan type inputs for $answers") cask.Response( planTypesInputsHtml, headers = Seq("Content-Type" -> "text/html;charset=UTF-8") ) } } } @cask.staticResources("/public") def publicFiles() = "public" initialize() }