feat(15): steps submit form and set next

This commit is contained in:
efim 2023-07-14 20:17:15 +00:00
parent 65bfe3077f
commit febc77032b
6 changed files with 168 additions and 37 deletions

View File

@ -40,6 +40,7 @@
id="form-step"
th:fragment="formFragment(formData)"
hx-post="/submit-step/1"
hx-swap="outerHTML"
action="/submit-step/1"
method="post"
>

View File

@ -39,6 +39,10 @@
<form
class="flex flex-col items-center w-screen h-screen md:grid md:items-start md:p-5 md:bg-white md:rounded-2xl md:grid-cols-[auto_1fr] md:w-desktop-form md:h-desktop-form md:drop-shadow-2xl"
id="form-step"
hx-post="/submit-step/2"
hx-swap="outerHTML"
action="/submit-step/2"
method="post"
th:fragment="formFragment(formData)"
>
<summary
@ -183,7 +187,13 @@
>Go Back</a
>
<div class="grow"></div>
<input
type="submit"
class="grid place-content-center mr-3 w-24 h-10 text-sm font-semibold text-white rounded md:mr-24 md:w-32 md:h-12 md:text-base md:rounded-lg bg-marine-blue"
value="Next Step"
>
<a
th:remove="all"
href="step3.html"
class="grid place-content-center mr-3 w-24 h-10 text-sm font-semibold text-white rounded md:mr-24 md:w-32 md:h-12 md:text-base md:rounded-lg bg-marine-blue"
>Next Step</a

View File

@ -39,6 +39,10 @@
<form
class="flex flex-col items-center w-screen h-screen md:grid md:items-start md:p-5 md:bg-white md:rounded-2xl md:grid-cols-[auto_1fr] md:w-desktop-form md:h-desktop-form md:drop-shadow-2xl"
id="form-step"
hx-post="/submit-step/3"
hx-swap="outerHTML"
action="/submit-step/3"
method="post"
th:fragment="formFragment(formData)"
>
<summary
@ -105,9 +109,7 @@
<p class="py-3 text-cool-gray">
Add-ons help enhance your gaming experience.
</p>
<div
class="flex flex-col w-full text-sm md:text-base"
>
<div class="flex flex-col w-full text-sm md:text-base">
<label for="multiplayer-games" class="relative pl-6 h-20 md:w-full">
<input
id="multiplayer-games"
@ -118,10 +120,14 @@
<div
class="absolute inset-y-0 inset-x-0 rounded-lg border border-cool-gray peer-checked:border-purplish-blue peer-checked:bg-magnolia/50"
>
<div class="grid place-content-center ml-20 h-full grid-cols-[1fr_100px]">
<div
class="grid place-content-center ml-20 h-full grid-cols-[1fr_100px]"
>
<h1>Online Service</h1>
<p>Access to multiplayer games</p>
<p class="col-start-2 row-span-2 row-start-1 self-center">+$1/mo</p>
<p class="col-start-2 row-span-2 row-start-1 self-center">
+$1/mo
</p>
</div>
</div>
</label>
@ -136,10 +142,14 @@
<div
class="absolute inset-y-0 inset-x-0 rounded-lg border border-cool-gray peer-checked:border-purplish-blue peer-checked:bg-magnolia/50"
>
<div class="grid place-content-center ml-20 h-full grid-cols-[1fr_100px]">
<div
class="grid place-content-center ml-20 h-full grid-cols-[1fr_100px]"
>
<h1>Larger storage</h1>
<p>Extra 1TB of cloud save</p>
<p class="col-start-2 row-span-2 row-start-1 self-center">+$2/mo</p>
<p class="col-start-2 row-span-2 row-start-1 self-center">
+$2/mo
</p>
</div>
</div>
</label>
@ -154,10 +164,14 @@
<div
class="absolute inset-y-0 inset-x-0 rounded-lg border border-cool-gray peer-checked:border-purplish-blue peer-checked:bg-magnolia/50"
>
<div class="grid place-content-center ml-20 h-full grid-cols-[1fr_100px]">
<div
class="grid place-content-center ml-20 h-full grid-cols-[1fr_100px]"
>
<h1>Customizable Profile</h1>
<p>Custom theme on your profile</p>
<p class="col-start-2 row-span-2 row-start-1 self-center">+$2/mo</p>
<p class="col-start-2 row-span-2 row-start-1 self-center">
+$2/mo
</p>
</div>
</div>
</label>
@ -176,7 +190,14 @@
>Go Back</a
>
<div class="grow"></div>
<input
type="submit"
href="step4.html"
class="grid place-content-center mr-3 w-24 h-10 text-sm font-semibold text-white rounded md:mr-24 md:w-32 md:h-12 md:text-base md:rounded-lg bg-marine-blue"
value="Next Step"
/>
<a
th:remove="all"
href="step4.html"
class="grid place-content-center mr-3 w-24 h-10 text-sm font-semibold text-white rounded md:mr-24 md:w-32 md:h-12 md:text-base md:rounded-lg bg-marine-blue"
>Next Step</a

View File

@ -39,6 +39,10 @@
<form
class="flex flex-col items-center w-screen h-screen md:grid md:items-start md:p-5 md:bg-white md:rounded-2xl md:grid-cols-[auto_1fr] md:w-desktop-form md:h-desktop-form md:drop-shadow-2xl"
id="form-step"
hx-post="/submit-step/4"
action="/submit-step/4"
hx-swap="outerHTML"
method="post"
th:fragment="formFragment(formData)"
>
<summary
@ -135,7 +139,13 @@
>Go Back</a
>
<div class="grow"></div>
<input
type="submit"
class="grid place-content-center mr-3 w-24 h-10 text-sm font-semibold text-white rounded md:mr-24 md:w-32 md:h-12 md:text-base md:rounded-lg bg-purplish-blue"
value="Confirm"
>
<a
th:remove="all"
href="step5.html"
class="grid place-content-center mr-3 w-24 h-10 text-sm font-semibold text-white rounded md:mr-24 md:w-32 md:h-12 md:text-base md:rounded-lg bg-purplish-blue"
>Confirm</a

View File

@ -6,8 +6,77 @@ object Models {
final case class Answers(
sessionId: String = "id1",
currentStep: Int = 1,
// fragmentName: String = "step1",
step1: StepAnswers.Step1 = StepAnswers.Step1(),
step2: StepAnswers.Step2 = StepAnswers.Step2(),
step3: StepAnswers.Step3 = StepAnswers.Step3()
) {
// this is not enforced by compiler, sad, maintain by hand in html template files
def fragmentName: String = s"step${currentStep}"
def updateStep(stepNum: Int, rawData: String): Answers = {
stepNum match {
case 1 => this.copy(
step1 = this.step1.fromFormData(rawData),
currentStep = stepNum + 1
)
case 2 => this.copy(
step2 = this.step2.fromFormData(rawData),
currentStep = stepNum + 1
)
case 3 => this.copy(
step3 = this.step3.fromFormData(rawData),
currentStep = stepNum + 1
)
case 4 => this.copy(
currentStep = stepNum + 1
)
case _ => this
}
}
}
enum PlanType:
case Arcade, Advanced, Pro
enum Addons:
case OnlineService, LargerStorage, CustomProfile
sealed trait StepAnswers {
def fromFormData(rawData: String): StepAnswers
}
object StepAnswers {
final case class Step1(
name: String = "",
email: String = "",
phone: String = ""
) extends StepAnswers {
override def fromFormData(rawData: String): Step1 = {
val fieldValues = rawData.split("&").map { field =>
val Array(name, value) = field.split("=")
name -> value
}.toMap
val name = fieldValues.getOrElse("name", "")
val email = fieldValues.getOrElse("email", "")
val phone = fieldValues.getOrElse("phone", "")
Step1(name, email, phone)
}
}
final case class Step2(
planType: PlanType = PlanType.Arcade,
isYearly: Boolean = false
) extends StepAnswers {
override def fromFormData(rawData: String): Step2 = {
val a = 1
Step2()
}
}
final case class Step3(addons: Set[Addons] = Set.empty)
extends StepAnswers {
override def fromFormData(rawData: String): Step3 = {
val a = 1
Step3()
}
}
}
}

View File

@ -19,16 +19,23 @@ case class Routes()(implicit cc: castor.Context, log: cask.Logger)
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
/** 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 None =>
Seq(
cask.Cookie(
sessoinCookieName,
UUID.randomUUID().toString(),
path = "/"
)
)
case Some(_) => Seq.empty // don't set new cookies
}
@ -45,7 +52,8 @@ case class Routes()(implicit cc: castor.Context, log: cask.Logger)
@cask.get("/force-new-session")
def forceNewSession() = {
val newSessionCookie = cask.Cookie(sessoinCookieName, UUID.randomUUID().toString(), path = "/")
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",
@ -54,18 +62,21 @@ case class Routes()(implicit cc: castor.Context, log: cask.Logger)
)
}
/**
* 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
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 state = Models.Answers(currentStep = 1)
cask.Response("yoyo")
val context = new Context()
context.setVariable("formData", state)
val formFragment = templateEngine.process(state.fragmentName, Set("formFragment").asJava, context)
context.setVariable(formDataContextVarName, state)
val formFragment = templateEngine.process(
state.fragmentName,
Set("formFragment").asJava,
context
)
cask.Response(
formFragment,
headers = Seq("Content-Type" -> "text/html;charset=UTF-8")
@ -74,20 +85,29 @@ case class Routes()(implicit cc: castor.Context, log: cask.Logger)
// i guess let's make step a hidden input?
@cask.post("/submit-step/:stepNum")
def submitStep(sessionId: cask.Cookie, stepNum: Int, request: cask.Request) = {
def submitStep(
sessionId: cask.Cookie,
stepNum: Int,
request: cask.Request
) = {
val id = sessionId.value
println(s"got $request for $id")
println(s"got $request for $id. it's data is ${request.text()}")
val userAnswers = Sessions.sessionReplies.getOrElse(id, Answers(id))
// now i want to do what?
// select 'answerStep' form userAnswers
// delegate parsing and saving of the request.text to it
// set current step to next
// and return next rendered form step
// with the 'form data' caclulated from the updated answers
cask.Response(s"what i got is $request and ${request.text()}")
val updatedAnswers = userAnswers.updateStep(stepNum, request.text())
val context = new Context()
context.setVariable(formDataContextVarName, updatedAnswers)
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")
)
}
@cask.staticResources("/public")