Compare commits

...

20 Commits

Author SHA1 Message Date
efim 0e7d53b582 feat(15): stylings of steps 1-3
and mobile styling of step 4
2023-07-15 20:19:54 +00:00
efim 96edd50450 feat(15): adding only required validation 2023-07-15 17:31:25 +00:00
efim dd590e1530 feat(15): jump from step4 to step2
here hidden 'form-confirmed' input will result in 'submitted' attribute
of Step4 answer to be set,
but this field is explicitly excluded on the hx-post from the Back
and Change links, this seems OK, i guess!
2023-07-15 17:15:21 +00:00
efim 3133a9aa8c feat(15): dynamicly redrawing plan type info yr/mo 2023-07-15 15:46:32 +00:00
efim a52ab42c61 feat(15): steps summary as dynamic fragment
defined in step1, reused in other steps
2023-07-15 14:35:52 +00:00
efim 77e70c3536 fix(15): set skipped identical prices
so 150 + 10 + 20 + 20 was only 180
2023-07-15 13:49:51 +00:00
efim 4d9a7c3e12 feat(15): dynamic values in step 3 addon selection 2023-07-15 13:48:48 +00:00
efim 6d827365ac feat(15): dynamic rendering of plan types
currently only on page revisit.
for dynamic - need to use htmx swapping, with separate endpoing getting
updates value from whole element being a fragment
2023-07-15 13:21:26 +00:00
efim f9c32fd7dc feat(15): putting state values into summary step 2023-07-15 12:55:22 +00:00
efim 998cc778e6 feat(15): adding dynamic form data source 2023-07-15 12:05:40 +00:00
efim 076dc76ca4 feat(15): submitting form data on step back 2023-07-15 12:05:04 +00:00
efim 5f260455cb feat(15): displaying existing answers in form step 2023-07-15 04:36:28 +00:00
efim bf33858e41 feat(15): route to get previous form step 2023-07-15 03:50:51 +00:00
efim 6ed489835f feat(15): start form, save state, decode steps 2023-07-14 20:49:00 +00:00
efim febc77032b feat(15): steps submit form and set next 2023-07-14 20:17:15 +00:00
efim 65bfe3077f feat(15): receiving form submission from step 1 2023-07-14 18:29:25 +00:00
efim f8f1580fc7 feat(15): making fragments, loading based on step 2023-07-14 05:03:18 +00:00
efim aca7ae8ecf feat(15): index placeholder for the form
to be htmx'ed into getting appropriate step for the session
2023-07-13 19:31:53 +00:00
efim 03aa9f8b94 feat(15): associating session with root page 2023-07-13 18:31:30 +00:00
efim 2d514e9258 feat(15): initial styling step 5 2023-07-13 06:15:53 +00:00
13 changed files with 1248 additions and 176 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,32 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient x1="8.042%" y1="0%" x2="65.682%" y2="23.865%" id="a">
<stop stop-color="hsl(228, 100%, 84%)" stop-opacity="0" offset="0%"/>
<stop stop-color="hsl(228, 100%, 84%)" stop-opacity=".631" offset="63.146%"/>
<stop stop-color="hsl(228, 100%, 84%)" offset="100%"/>
</linearGradient>
</defs>
<g fill="none" fill-rule="evenodd">
<g transform="translate(1 1)">
<path d="M36 18c0-9.94-8.06-18-18-18" id="Oval-2" stroke="url(#a)" stroke-width="2">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="0.9s"
repeatCount="indefinite" />
</path>
<circle fill="#fff" cx="36" cy="18" r="1">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="0.9s"
repeatCount="indefinite" />
</circle>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1 +0,0 @@
hello from the resource file!

View File

@ -548,30 +548,26 @@ video {
bottom: 0px; bottom: 0px;
} }
.z-0 { .end-0 {
z-index: 0; inset-inline-end: 0px;
} }
.z-10 { .top-0 {
z-index: 10; top: 0px;
}
.z-20 {
z-index: 20;
} }
.z-40 { .z-40 {
z-index: 40; z-index: 40;
} }
.z-50 {
z-index: 50;
}
.col-start-2 { .col-start-2 {
grid-column-start: 2; grid-column-start: 2;
} }
.row-span-full {
grid-row: 1 / -1;
}
.row-span-2 { .row-span-2 {
grid-row: span 2 / span 2; grid-row: span 2 / span 2;
} }
@ -580,26 +576,8 @@ video {
grid-row-start: 1; grid-row-start: 1;
} }
.m-8 { .m-3 {
margin: 2rem; margin: 0.75rem;
}
.m-4 {
margin: 1rem;
}
.m-6 {
margin: 1.5rem;
}
.my-10 {
margin-top: 2.5rem;
margin-bottom: 2.5rem;
}
.my-8 {
margin-top: 2rem;
margin-bottom: 2rem;
} }
.my-7 { .my-7 {
@ -607,14 +585,47 @@ video {
margin-bottom: 1.75rem; margin-bottom: 1.75rem;
} }
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
.mx-3 {
margin-left: 0.75rem;
margin-right: 0.75rem;
}
.mx-9 {
margin-left: 2.25rem;
margin-right: 2.25rem;
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.my-5 {
margin-top: 1.25rem;
margin-bottom: 1.25rem;
}
.-mt-20 { .-mt-20 {
margin-top: -5rem; margin-top: -5rem;
} }
.mb-2 {
margin-bottom: 0.5rem;
}
.ml-2 { .ml-2 {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
.ml-20 {
margin-left: 5rem;
}
.ml-6 { .ml-6 {
margin-left: 1.5rem; margin-left: 1.5rem;
} }
@ -631,20 +642,36 @@ video {
margin-top: 0.3rem; margin-top: 0.3rem;
} }
.ml-\[200px\] { .mt-3 {
margin-left: 200px; margin-top: 0.75rem;
} }
.ml-\[100px\] { .mb-4 {
margin-left: 100px; margin-bottom: 1rem;
} }
.ml-\[70px\] { .mt-2 {
margin-left: 70px; margin-top: 0.5rem;
} }
.ml-20 { .ml-16 {
margin-left: 5rem; margin-left: 4rem;
}
.mb-10 {
margin-bottom: 2.5rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
.mt-4 {
margin-top: 1rem;
}
.mt-5 {
margin-top: 1.25rem;
} }
.flex { .flex {
@ -671,6 +698,10 @@ video {
height: 3rem; height: 3rem;
} }
.h-14 {
height: 3.5rem;
}
.h-20 { .h-20 {
height: 5rem; height: 5rem;
} }
@ -687,34 +718,42 @@ video {
height: 1.25rem; height: 1.25rem;
} }
.h-8 { .h-6 {
height: 2rem; height: 1.5rem;
} }
.h-screen { .h-8 {
height: 100vh; height: 2rem;
} }
.h-full { .h-full {
height: 100%; height: 100%;
} }
.h-6 { .h-screen {
height: 1.5rem; height: 100vh;
} }
.h-4 { .h-16 {
height: 1rem; height: 4rem;
} }
.w-11\/12 { .w-11\/12 {
width: 91.666667%; width: 91.666667%;
} }
.w-14 {
width: 3.5rem;
}
.w-24 { .w-24 {
width: 6rem; width: 6rem;
} }
.w-6 {
width: 1.5rem;
}
.w-8 { .w-8 {
width: 2rem; width: 2rem;
} }
@ -731,14 +770,6 @@ video {
width: 100vw; width: 100vw;
} }
.w-6 {
width: 1.5rem;
}
.w-4 {
width: 1rem;
}
.grow { .grow {
flex-grow: 1; flex-grow: 1;
} }
@ -757,12 +788,28 @@ video {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
.grid-cols-\[1fr_100px\] {
grid-template-columns: 1fr 100px;
}
.grid-cols-\[auto_1fr\] {
grid-template-columns: auto 1fr;
}
.grid-cols-\[repeat\(4\2c _auto\)\] { .grid-cols-\[repeat\(4\2c _auto\)\] {
grid-template-columns: repeat(4, auto); grid-template-columns: repeat(4, auto);
} }
.grid-cols-\[1fr_100px\] { .grid-cols-\[1fr_auto\] {
grid-template-columns: 1fr 100px; grid-template-columns: 1fr auto;
}
.grid-cols-\[1fr_50px\] {
grid-template-columns: 1fr 50px;
}
.grid-cols-\[1fr_70px\] {
grid-template-columns: 1fr 70px;
} }
.flex-row { .flex-row {
@ -789,6 +836,10 @@ video {
align-items: center; align-items: center;
} }
.justify-items-center {
justify-items: center;
}
.gap-x-5 { .gap-x-5 {
-moz-column-gap: 1.25rem; -moz-column-gap: 1.25rem;
column-gap: 1.25rem; column-gap: 1.25rem;
@ -798,18 +849,26 @@ video {
row-gap: 1rem; row-gap: 1rem;
} }
.gap-y-3 {
row-gap: 0.75rem;
}
.divide-y > :not([hidden]) ~ :not([hidden]) { .divide-y > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0; --tw-divide-y-reverse: 0;
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
} }
.self-center { .place-self-center {
align-self: center; place-self: center;
} }
.justify-self-center { .self-end {
justify-self: center; align-self: flex-end;
}
.self-center {
align-self: center;
} }
.rounded { .rounded {
@ -842,16 +901,16 @@ video {
border-color: rgb(255 255 255 / var(--tw-border-opacity)); border-color: rgb(255 255 255 / var(--tw-border-opacity));
} }
.border-light-gray {
--tw-border-opacity: 1;
border-color: hsl(229 24% 87% / var(--tw-border-opacity));
}
.bg-green-200 { .bg-green-200 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(187 247 208 / var(--tw-bg-opacity)); background-color: rgb(187 247 208 / var(--tw-bg-opacity));
} }
.bg-light-gray {
--tw-bg-opacity: 1;
background-color: hsl(229 24% 87% / var(--tw-bg-opacity));
}
.bg-magnolia { .bg-magnolia {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: hsl(217 100% 97% / var(--tw-bg-opacity)); background-color: hsl(217 100% 97% / var(--tw-bg-opacity));
@ -862,48 +921,76 @@ video {
background-color: hsl(213 96% 18% / var(--tw-bg-opacity)); background-color: hsl(213 96% 18% / var(--tw-bg-opacity));
} }
.bg-purplish-blue {
--tw-bg-opacity: 1;
background-color: hsl(243 100% 62% / var(--tw-bg-opacity));
}
.bg-white { .bg-white {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity));
} }
.bg-yellow-500 { .\!bg-red-500 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1 !important;
background-color: rgb(234 179 8 / var(--tw-bg-opacity)); background-color: rgb(239 68 68 / var(--tw-bg-opacity)) !important;
} }
.bg-cool-gray { .\!bg-white {
--tw-bg-opacity: 1; --tw-bg-opacity: 1 !important;
background-color: hsl(231 11% 63% / var(--tw-bg-opacity)); background-color: rgb(255 255 255 / var(--tw-bg-opacity)) !important;
} }
.bg-light-blue { .\!bg-light-blue {
--tw-bg-opacity: 1; --tw-bg-opacity: 1 !important;
background-color: hsl(206 94% 87% / var(--tw-bg-opacity)); background-color: hsl(206 94% 87% / var(--tw-bg-opacity)) !important;
} }
.bg-pastel-blue { .bg-light-gray {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: hsl(228 100% 84% / var(--tw-bg-opacity)); background-color: hsl(229 24% 87% / var(--tw-bg-opacity));
} }
.bg-purplish-blue { .bg-light-gray\/50 {
--tw-bg-opacity: 1; background-color: hsl(229 24% 87% / 0.5);
background-color: hsl(243 100% 62% / var(--tw-bg-opacity)); }
.bg-light-gray\/25 {
background-color: hsl(229 24% 87% / 0.25);
}
.bg-magnolia\/75 {
background-color: hsl(217 100% 97% / 0.75);
} }
.bg-sidebar-mobile { .bg-sidebar-mobile {
background-image: url("images/bg-sidebar-mobile.svg"); background-image: url("images/bg-sidebar-mobile.svg");
} }
.bg-auto {
background-size: auto;
}
.bg-cover {
background-size: cover;
}
.bg-no-repeat { .bg-no-repeat {
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.fill-current {
fill: currentColor;
}
.p-1 { .p-1 {
padding: 0.25rem; padding: 0.25rem;
} }
.p-3 {
padding: 0.75rem;
}
.px-4 { .px-4 {
padding-left: 1rem; padding-left: 1rem;
padding-right: 1rem; padding-right: 1rem;
@ -914,6 +1001,11 @@ video {
padding-right: 1.5rem; padding-right: 1.5rem;
} }
.py-20 {
padding-top: 5rem;
padding-bottom: 5rem;
}
.py-3 { .py-3 {
padding-top: 0.75rem; padding-top: 0.75rem;
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
@ -929,20 +1021,33 @@ video {
padding-bottom: 2rem; padding-bottom: 2rem;
} }
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.pl-6 {
padding-left: 1.5rem;
}
.pt-3 { .pt-3 {
padding-top: 0.75rem; padding-top: 0.75rem;
} }
.pl-8 { .pl-4 {
padding-left: 2rem; padding-left: 1rem;
} }
.pl-5 { .pl-5 {
padding-left: 1.25rem; padding-left: 1.25rem;
} }
.pl-6 { .pb-5 {
padding-left: 1.5rem; padding-bottom: 1.25rem;
}
.text-center {
text-align: center;
} }
.text-2xl { .text-2xl {
@ -960,6 +1065,11 @@ video {
line-height: 1.25rem; line-height: 1.25rem;
} }
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.font-bold { .font-bold {
font-weight: 700; font-weight: 700;
} }
@ -977,6 +1087,11 @@ video {
color: hsl(231 11% 63% / var(--tw-text-opacity)); color: hsl(231 11% 63% / var(--tw-text-opacity));
} }
.text-green-500 {
--tw-text-opacity: 1;
color: rgb(34 197 94 / var(--tw-text-opacity));
}
.text-light-gray { .text-light-gray {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: hsl(229 24% 87% / var(--tw-text-opacity)); color: hsl(229 24% 87% / var(--tw-text-opacity));
@ -992,6 +1107,50 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));
} }
.\!text-marine-blue {
--tw-text-opacity: 1 !important;
color: hsl(213 96% 18% / var(--tw-text-opacity)) !important;
}
.text-purplish-blue {
--tw-text-opacity: 1;
color: hsl(243 100% 62% / var(--tw-text-opacity));
}
.text-magnolia {
--tw-text-opacity: 1;
color: hsl(217 100% 97% / var(--tw-text-opacity));
}
.text-alabaster {
--tw-text-opacity: 1;
color: hsl(231 100% 99% / var(--tw-text-opacity));
}
.underline {
text-decoration-line: underline;
}
.accent-green-500 {
accent-color: #22c55e;
}
.accent-marine-blue {
accent-color: hsl(213, 96%, 18%);
}
.accent-pastel-blue {
accent-color: hsl(228, 100%, 84%);
}
.accent-light-blue {
accent-color: hsl(206, 94%, 87%);
}
.accent-purplish-blue {
accent-color: hsl(243, 100%, 62%);
}
.drop-shadow-xl { .drop-shadow-xl {
--tw-drop-shadow: drop-shadow(0 20px 13px rgb(0 0 0 / 0.03)) drop-shadow(0 8px 5px rgb(0 0 0 / 0.08)); --tw-drop-shadow: drop-shadow(0 20px 13px rgb(0 0 0 / 0.03)) drop-shadow(0 8px 5px rgb(0 0 0 / 0.08));
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
@ -1111,15 +1270,105 @@ html {
transition-duration: 150ms; transition-duration: 150ms;
} }
.checked\:bg-marine-blue:checked {
--tw-bg-opacity: 1;
background-color: hsl(213 96% 18% / var(--tw-bg-opacity));
}
.checked\:bg-blue-500:checked {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}
.checked\:after\:ml-\[1\.2rem\]:checked::after { .checked\:after\:ml-\[1\.2rem\]:checked::after {
content: var(--tw-content); content: var(--tw-content);
margin-left: 1.2rem; margin-left: 1.2rem;
} }
.invalid\:border-strawberry-red:invalid {
--tw-border-opacity: 1;
border-color: hsl(354 84% 57% / var(--tw-border-opacity));
}
.invalid\:border-red-500:invalid {
--tw-border-opacity: 1;
border-color: rgb(239 68 68 / var(--tw-border-opacity));
}
.invalid\:border-green-500:invalid {
--tw-border-opacity: 1;
border-color: rgb(34 197 94 / var(--tw-border-opacity));
}
.hover\:cursor-pointer:hover { .hover\:cursor-pointer:hover {
cursor: pointer; cursor: pointer;
} }
.hover\:border-purplish-blue:hover {
--tw-border-opacity: 1;
border-color: hsl(243 100% 62% / var(--tw-border-opacity));
}
.hover\:border-green-700:hover {
--tw-border-opacity: 1;
border-color: rgb(21 128 61 / var(--tw-border-opacity));
}
.hover\:border-red-700:hover {
--tw-border-opacity: 1;
border-color: rgb(185 28 28 / var(--tw-border-opacity));
}
.hover\:bg-magnolia\/25:hover {
background-color: hsl(217 100% 97% / 0.25);
}
.hover\:bg-magnolia\/\[10\]:hover {
background-color: hsl(217 100% 97% / 10);
}
.hover\:bg-magnolia\/\[\.1\]:hover {
background-color: hsl(217 100% 97% / .1);
}
.hover\:bg-red-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
}
.focus\:border-green-500:focus {
--tw-border-opacity: 1;
border-color: rgb(34 197 94 / var(--tw-border-opacity));
}
.focus\:border-marine-blue:focus {
--tw-border-opacity: 1;
border-color: hsl(213 96% 18% / var(--tw-border-opacity));
}
.focus\:outline-none:focus {
outline: 2px solid transparent;
outline-offset: 2px;
}
.focus\:outline-marine-blue:focus {
outline-color: hsl(213, 96%, 18%);
}
.focus\:outline-green-500:focus {
outline-color: #22c55e;
}
.active\:border-red-500:active {
--tw-border-opacity: 1;
border-color: rgb(239 68 68 / var(--tw-border-opacity));
}
.active\:bg-red-500:active {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.peer:checked ~ .peer-checked\:border-purplish-blue { .peer:checked ~ .peer-checked\:border-purplish-blue {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: hsl(243 100% 62% / var(--tw-border-opacity)); border-color: hsl(243 100% 62% / var(--tw-border-opacity));
@ -1134,8 +1383,16 @@ html {
background-color: hsl(217 100% 97% / 0.5); background-color: hsl(217 100% 97% / 0.5);
} }
.peer:checked ~ .peer-checked\:bg-magnolia\/20 { .peer:checked ~ .peer-checked\:bg-magnolia\/75 {
background-color: hsl(217 100% 97% / 0.2); background-color: hsl(217 100% 97% / 0.75);
}
.peer:checked ~ .peer-checked\:bg-magnolia\/25 {
background-color: hsl(217 100% 97% / 0.25);
}
.peer:checked ~ .peer-checked\:bg-magnolia\/\[\.1\] {
background-color: hsl(217 100% 97% / .1);
} }
.peer:checked ~ .peer-checked\:text-cool-gray { .peer:checked ~ .peer-checked\:text-cool-gray {
@ -1153,6 +1410,30 @@ html {
grid-row: span 2 / span 2; grid-row: span 2 / span 2;
} }
.md\:row-span-1 {
grid-row: span 1 / span 1;
}
.md\:mx-10 {
margin-left: 2.5rem;
margin-right: 2.5rem;
}
.md\:mx-4 {
margin-left: 1rem;
margin-right: 1rem;
}
.md\:my-10 {
margin-top: 2.5rem;
margin-bottom: 2.5rem;
}
.md\:my-7 {
margin-top: 1.75rem;
margin-bottom: 1.75rem;
}
.md\:ml-20 { .md\:ml-20 {
margin-left: 5rem; margin-left: 5rem;
} }
@ -1165,6 +1446,22 @@ html {
margin-top: 0px; margin-top: 0px;
} }
.md\:ml-8 {
margin-left: 2rem;
}
.md\:ml-10 {
margin-left: 2.5rem;
}
.md\:ml-24 {
margin-left: 6rem;
}
.md\:mt-5 {
margin-top: 1.25rem;
}
.md\:flex { .md\:flex {
display: flex; display: flex;
} }
@ -1193,6 +1490,30 @@ html {
height: 100%; height: 100%;
} }
.md\:h-32 {
height: 8rem;
}
.md\:h-40 {
height: 10rem;
}
.md\:h-10 {
height: 2.5rem;
}
.md\:h-16 {
height: 4rem;
}
.md\:h-20 {
height: 5rem;
}
.md\:h-24 {
height: 6rem;
}
.md\:w-32 { .md\:w-32 {
width: 8rem; width: 8rem;
} }
@ -1209,8 +1530,8 @@ html {
width: 100%; width: 100%;
} }
.md\:w-20 { .md\:grow {
width: 5rem; flex-grow: 1;
} }
.md\:grid-cols-1 { .md\:grid-cols-1 {
@ -1225,6 +1546,10 @@ html {
grid-template-rows: repeat(4, auto); grid-template-rows: repeat(4, auto);
} }
.md\:grid-rows-\[1fr_auto_auto\] {
grid-template-rows: 1fr auto auto;
}
.md\:flex-row { .md\:flex-row {
flex-direction: row; flex-direction: row;
} }
@ -1245,6 +1570,10 @@ html {
align-items: flex-end; align-items: flex-end;
} }
.md\:justify-between {
justify-content: space-between;
}
.md\:gap-x-4 { .md\:gap-x-4 {
-moz-column-gap: 1rem; -moz-column-gap: 1rem;
column-gap: 1rem; column-gap: 1rem;
@ -1254,6 +1583,14 @@ html {
row-gap: 1.75rem; row-gap: 1.75rem;
} }
.md\:gap-y-4 {
row-gap: 1rem;
}
.md\:place-self-start {
place-self: start;
}
.md\:rounded-2xl { .md\:rounded-2xl {
border-radius: 1rem; border-radius: 1rem;
} }
@ -1283,6 +1620,10 @@ html {
padding: 1.5rem; padding: 1.5rem;
} }
.md\:p-4 {
padding: 1rem;
}
.md\:px-24 { .md\:px-24 {
padding-left: 6rem; padding-left: 6rem;
padding-right: 6rem; padding-right: 6rem;
@ -1293,6 +1634,16 @@ html {
padding-right: 1rem; padding-right: 1rem;
} }
.md\:py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.md\:px-0 {
padding-left: 0px;
padding-right: 0px;
}
.md\:pb-2 { .md\:pb-2 {
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
@ -1305,6 +1656,10 @@ html {
padding-top: 1.25rem; padding-top: 1.25rem;
} }
.md\:pb-10 {
padding-bottom: 2.5rem;
}
.md\:text-4xl { .md\:text-4xl {
font-size: 2.25rem; font-size: 2.25rem;
line-height: 2.5rem; line-height: 2.5rem;
@ -1315,6 +1670,21 @@ html {
line-height: 1.5rem; line-height: 1.5rem;
} }
.md\:text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.md\:text-6xl {
font-size: 3.75rem;
line-height: 1;
}
.md\:text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
}
.md\:drop-shadow-2xl { .md\:drop-shadow-2xl {
--tw-drop-shadow: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15)); --tw-drop-shadow: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15));
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);

View File

@ -9,10 +9,15 @@
rel="icon" rel="icon"
type="image/png" type="image/png"
sizes="32x32" sizes="32x32"
href="./assets/images/favicon-32x32.png" th:href="'public/images/favicon-32x32.png'"
href="../public/images/favicon-32x32.png"
/> />
<link href="../public/out.css" rel="stylesheet" /> <link
th:href="'public/out.css'"
href="../public/out.css"
rel="stylesheet"
/>
<script th:src="'public/htmx.min.js'" src="../public/htmx.min.js"></script>
<title>Frontend Mentor | Multi-step form</title> <title>Frontend Mentor | Multi-step form</title>
<!-- Feel free to remove these styles or customise in your own stylesheet 👍 --> <!-- Feel free to remove these styles or customise in your own stylesheet 👍 -->
@ -27,7 +32,25 @@
</style> </style>
</head> </head>
<body> <body>
<main class="bg-light-gray h-screen w-screen"> <main class="grid place-content-center w-screen h-screen bg-magnolia">
<section
hx-get="/get-form"
hx-trigger="load"
hx-swap="outerHTML"
>
<!-- here be immediate hx-get for the form. to subscitute the body --> <!-- here be immediate hx-get for the form. to subscitute the body -->
<img class="w-14 text-green-500 fill-current" th:src="'public/images/tail-spin.svg'" src="../public/images/tail-spin.svg" alt="loading..." />
</section>
<section class="absolute top-0 end-0" id="new-session-control">
<button hx-get="/force-new-session">Request new session</button>
</section>
<div class="fixed inset-x-0 bottom-0 attribution">
Challenge by
<a href="https://www.frontendmentor.io?ref=challenge" target="_blank"
>Frontend Mentor</a
>. Coded by <a href="#">Your Name Here</a>.
</div>
</main>
</body> </body>
</html> </html>

View File

@ -35,55 +35,82 @@
</p> </p>
<![endif]--> <![endif]-->
<article <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" 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" id="form-step"
th:fragment="formFragment(formData)"
hx-post="/submit-step/1/2"
hx-swap="outerHTML"
action="/submit-step/1/2"
method="post"
> >
<summary <summary
class="w-full h-44 bg-no-repeat md:row-span-2 bg-sidebar-mobile marker:text-white md:bg-sidebar-desktop md:h-[568px] md:w-[274px]" class="w-full h-44 bg-no-repeat bg-cover md:row-span-2 bg-sidebar-mobile marker:text-white md:bg-sidebar-desktop md:h-[568px] md:w-[274px]"
id="sidebar" id="sidebar"
th:fragment="stepsSummary(formData)"
> >
<ol <ol
class="grid grid-cols-[repeat(4,_auto)] gap-x-5 content-center items-center place-content-center h-24 md:flex-col md:h-full md:grid-rows-[repeat(4,_auto)] md:grid-cols-1 md:content-start md:p-10 md:gap-y-7 text-white text-sm uppercase" class="grid grid-cols-[repeat(4,_auto)] gap-x-5 content-center items-center place-content-center h-24 md:flex-col md:h-full md:grid-rows-[repeat(4,_auto)] md:grid-cols-1 md:content-start md:p-10 md:gap-y-7 text-white text-sm uppercase"
> >
<li class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]"> <li
class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]"
th:each="stepNum: ${formData.stepsAmount}"
>
<div <div
class="grid place-content-center w-8 h-8 text-white rounded-full border border-white" class="grid place-content-center w-8 h-8 font-bold text-white rounded-full border border-white"
th:classappend="${stepNum.index} == ${formData.userAnswers.currentStep} ? '!bg-light-blue !text-marine-blue'"
th:text="${stepNum.index}"
> >
1 1
</div> </div>
<p class="hidden md:flex md:flex-col"> <p class="hidden md:flex md:flex-col">
<span class="text-light-gray">Step 1</span><span class="font-bold">Your info</span> <span class="text-light-gray"
>Step <span th:text="${stepNum.index}">1</span></span
><span class="font-bold" th:text="${stepNum.name}"
>Your info</span
>
</p> </p>
</li> </li>
<li class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]"> <li
class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]"
th:remove="all"
>
<div <div
class="grid place-content-center w-8 h-8 text-white rounded-full border border-white" class="grid place-content-center w-8 h-8 text-white rounded-full border border-white"
> >
2 2
</div> </div>
<p class="hidden md:flex md:flex-col"> <p class="hidden md:flex md:flex-col">
<span class="text-light-gray">Step 2</span><span class="font-bold">Select plan</span> <span class="text-light-gray">Step 2</span
><span class="font-bold">Select plan</span>
</p> </p>
</li> </li>
<li class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]"> <li
class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]"
th:remove="all"
>
<div <div
class="grid place-content-center w-8 h-8 text-white rounded-full border border-white" class="grid place-content-center w-8 h-8 text-white rounded-full border border-white"
> >
3 3
</div> </div>
<p class="hidden md:flex md:flex-col"> <p class="hidden md:flex md:flex-col">
<span class="text-light-gray">Step 3</span><span class="font-bold">Add-ons</span> <span class="text-light-gray">Step 3</span
><span class="font-bold">Add-ons</span>
</p> </p>
</li> </li>
<li class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]"> <li
class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]"
th:remove="all"
>
<div <div
class="grid place-content-center w-8 h-8 text-white rounded-full border border-white" class="grid place-content-center w-8 h-8 text-white rounded-full border border-white"
> >
4 4
</div> </div>
<p class="hidden md:flex md:flex-col"> <p class="hidden md:flex md:flex-col">
<span class="text-light-gray">Step 4</span><span class="font-bold">summary</span> <span class="text-light-gray">Step 4</span
><span class="font-bold">summary</span>
</p> </p>
</li> </li>
</ol> </ol>
@ -93,34 +120,47 @@
class="flex flex-col py-8 px-6 -mt-20 w-11/12 bg-white rounded-xl md:px-24 md:mt-0 md:w-full drop-shadow-xl md:drop-shadow-none" class="flex flex-col py-8 px-6 -mt-20 w-11/12 bg-white rounded-xl md:px-24 md:mt-0 md:w-full drop-shadow-xl md:drop-shadow-none"
> >
<!-- Step 1 start --> <!-- Step 1 start -->
<h1 class="text-2xl font-bold md:text-4xl text-marine-blue">Personal info</h1> <h1 class="text-2xl font-bold md:text-4xl text-marine-blue">
Personal info
</h1>
<p class="py-3 text-cool-gray"> <p class="py-3 text-cool-gray">
Please provide your name, email address, and phone number. Please provide your name, email address, and phone number.
</p> </p>
<label for="name" class="pt-3 text-sm md:pt-5 md:pb-2 text-marine-blue">Name</label> <label for="name" class="pt-3 text-sm md:pt-5 md:pb-2 text-marine-blue"
>Name</label
>
<input <input
id="name" id="name"
th:value="${formData.userAnswers.step1.name}"
name="name"
type="text" type="text"
required
placeholder="e.g. Stephen King" placeholder="e.g. Stephen King"
class="p-1 px-4 h-10 text-sm font-semibold rounded border md:p-6 md:px-4 md:text-base md:rounded-lg placeholder:text-cool-gray" class="p-1 px-4 h-10 text-sm font-semibold rounded border md:p-6 md:px-4 md:text-base md:rounded-lg focus:outline-none placeholder:text-cool-gray invalid:border-strawberry-red focus:border-marine-blue"
/> />
<label for="email" class="pt-3 text-sm md:pt-5 md:pb-2 text-marine-blue" <label for="email" class="pt-3 text-sm md:pt-5 md:pb-2 text-marine-blue"
>Email Address</label >Email Address</label
> >
<input <input
id="email" id="email"
th:value="${formData.userAnswers.step1.email}"
name="email"
type="email" type="email"
required
placeholder="e.g. stephenking@lorem.com" placeholder="e.g. stephenking@lorem.com"
class="p-1 px-4 h-10 text-sm font-semibold rounded border md:p-6 md:px-4 md:text-base md:rounded-lg placeholder:text-cool-gray" class="p-1 px-4 h-10 text-sm font-semibold rounded border md:p-6 md:px-4 md:text-base md:rounded-lg focus:outline-none placeholder:text-cool-gray invalid:border-strawberry-red focus:border-marine-blue"
/> />
<label for="phone" class="pt-3 text-sm md:pt-5 md:pb-2 text-marine-blue" <label for="phone" class="pt-3 text-sm md:pt-5 md:pb-2 text-marine-blue"
>Phone Number</label >Phone Number</label
> >
<input <input
id="phone" id="phone"
th:value="${formData.userAnswers.step1.phone}"
name="phone"
type="tel" type="tel"
required
placeholder="e.g. +1 234 567 890" placeholder="e.g. +1 234 567 890"
class="p-1 px-4 h-10 text-sm font-semibold rounded border md:p-6 md:px-4 md:text-base md:rounded-lg placeholder:text-cool-gray" class="p-1 px-4 h-10 text-sm font-semibold rounded border md:p-6 md:px-4 md:text-base md:rounded-lg focus:outline-none placeholder:text-cool-gray invalid:border-strawberry-red focus:border-marine-blue"
/> />
<!-- Step 1 end --> <!-- Step 1 end -->
@ -131,13 +171,19 @@
class="flex flex-row items-center py-4 w-full bg-white md:items-end md:h-full" class="flex flex-row items-center py-4 w-full bg-white md:items-end md:h-full"
> >
<div class="grow"></div> <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 <a
th:remove="all"
href="step2.html" href="step2.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" 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 >Next Step</a
> >
</section> </section>
</article> </form>
<div class="fixed inset-x-0 bottom-0 attribution"> <div class="fixed inset-x-0 bottom-0 attribution">
Challenge by Challenge by
<a href="https://www.frontendmentor.io?ref=challenge" target="_blank" <a href="https://www.frontendmentor.io?ref=challenge" target="_blank"

View File

@ -36,13 +36,19 @@
<![endif]--> <![endif]-->
<main class="grid place-content-center h-screen"> <main class="grid place-content-center h-screen">
<article <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" 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" id="form-step"
hx-post="/submit-step/2/3"
hx-swap="outerHTML"
action="/submit-step/2/3"
method="post"
th:fragment="formFragment(formData)"
> >
<summary <summary
class="w-full h-44 bg-no-repeat md:row-span-2 bg-sidebar-mobile marker:text-white md:bg-sidebar-desktop md:h-[568px] md:w-[274px]" class="w-full h-44 bg-no-repeat md:row-span-2 bg-sidebar-mobile marker:text-white md:bg-sidebar-desktop md:h-[568px] md:w-[274px]"
id="sidebar" id="sidebar"
th:replace="~{step1::stepsSummary (${formData})}"
> >
<ol <ol
class="grid grid-cols-[repeat(4,_auto)] gap-x-5 content-center items-center place-content-center h-24 md:flex-col md:h-full md:grid-rows-[repeat(4,_auto)] md:grid-cols-1 md:content-start md:p-10 md:gap-y-7 text-white text-sm uppercase" class="grid grid-cols-[repeat(4,_auto)] gap-x-5 content-center items-center place-content-center h-24 md:flex-col md:h-full md:grid-rows-[repeat(4,_auto)] md:grid-cols-1 md:content-start md:p-10 md:gap-y-7 text-white text-sm uppercase"
@ -101,71 +107,120 @@
<h1 class="text-2xl font-bold md:text-4xl text-marine-blue"> <h1 class="text-2xl font-bold md:text-4xl text-marine-blue">
Select your plan Select your plan
</h1> </h1>
<p class="py-3 text-cool-gray"> <p class="py-3 mb-4 text-cool-gray">
You have the option of monthly or yearly billing. You have the option of monthly or yearly billing.
</p> </p>
<div <div
class="flex flex-col w-full md:flex-row" class="flex flex-col gap-y-3 my-3 w-full md:flex-row md:gap-x-4"
id="plan-type-inputs"
th:fragment="planTypesInputs(formData)"
>
<label
for="ArcadePlanType"
class="relative h-20 md:h-40 md:grow"
th:each="planType: ${formData.availablePlans}"
th:for="${planType}"
> >
<label for="ArcadePlanType" class="relative h-20 md:w-32">
<input <input
id="ArcadePlanType" id="ArcadePlanType"
th:id="${planType}"
type="radio" type="radio"
name="plan-type" name="plan-type"
value="Arcade" value="Arcade"
th:value="${planType}"
class="hidden peer" class="hidden peer"
th:checked="${formData.userAnswers.step2.planType.toString()} == ${planType.toString()}"
checked checked
/> />
<div <div
class="absolute inset-y-0 inset-x-0 rounded-lg border border-cool-gray peer-checked:border-purplish-blue peer-checked:bg-magnolia" class="grid absolute inset-y-0 inset-x-0 place-content-center h-full rounded-lg border md:grid-cols-1 md:p-4 grid-cols-[auto_1fr] border-light-gray peer-checked:border-purplish-blue peer-checked:bg-magnolia/50 md:grid-rows-[1fr_auto_auto] hover:bg-magnolia/[.1] hover:border-purplish-blue"
> >
<span class="">Arcade</span> <img
class="row-span-2 place-self-center px-4 md:row-span-1 md:place-self-start md:px-0"
th:src="${planType.iconPath}" src="../public/images/icon-arcade.svg" alt="" />
<h2 th:text="${planType}" class="font-semibold text-marine-blue">Arcade</h2>
<p
th:text="|$${formData.planCost(planType)}/${formData.periodCostLabel}|"
class="text-sm text-cool-gray"
>
$90/yr
</p>
<p th:if="${formData.userAnswers.step2.isYearly}" class="text-xs text-marine-blue">
2 months free
</p>
</div> </div>
</label> </label>
<label for="AdvancedPlanType" class="relative h-20 md:w-32"> <label
for="AdvancedPlanType"
class="relative h-20 md:w-32"
th:remove="all"
>
<input <input
id="AdvancedPlanType" id="AdvancedPlanType"
type="radio" type="radio"
name="plan-type" name="plan-type"
value="Advanced" value="Advanced"
class="hidden peer" class="hidden peer"
th:checked="${formData.userAnswers.step2.planType.toString()} == 'Advanced'"
/> />
<div <div
class="absolute inset-y-0 inset-x-0 rounded-lg border border-cool-gray peer-checked:border-purplish-blue peer-checked:bg-magnolia" class="absolute inset-y-0 inset-x-0 rounded-lg border border-cool-gray peer-checked:border-purplish-blue peer-checked:bg-magnolia"
> >
<span>Advanced</span> <img src="../public/images/icon-advanced.svg" alt="" />
<h2>Advanced</h2>
</div> </div>
</label> </label>
<label for="ProPlanType" class="relative h-20 md:w-32"> <label
for="ProPlanType"
class="relative h-20 md:w-32"
th:remove="all"
>
<input <input
id="ProPlanType" id="ProPlanType"
type="radio" type="radio"
name="plan-type" name="plan-type"
value="Pro" value="Pro"
class="hidden peer" class="hidden peer"
th:checked="${formData.userAnswers.step2.planType.toString()} == 'Pro'"
/> />
<div <div
class="absolute inset-y-0 inset-x-0 rounded-lg border border-cool-gray peer-checked:border-purplish-blue peer-checked:bg-magnolia" class="absolute inset-y-0 inset-x-0 rounded-lg border border-cool-gray peer-checked:border-purplish-blue peer-checked:bg-magnolia"
> >
<span>Pro</span> <img src="../public/images/icon-pro.svg" alt="" />
<h2>Pro</h2>
</div> </div>
</label> </label>
</div> </div>
<div class="grid grid-flow-col-dense place-content-center w-full rounded-lg bg-magnolia"> <div
class="grid grid-flow-col-dense place-content-center mt-3 w-full rounded-lg md:mt-5 bg-light-gray/25"
>
<div <div
class="inline-grid grid-cols-3 place-items-center h-12 text-sm font-bold" class="inline-grid grid-cols-3 place-items-center h-12 text-sm font-bold"
> >
<input <input
class="mr-2 w-9 h-5 ml-2 rounded-full appearance-none mt-[0.3rem] bg-marine-blue after:absolute after:h-3 after:w-3 after:rounded-full after:border-none after:bg-neutral-100 after:transition-[background-color_0.2s,transform_0.2s] checked:after:ml-[1.2rem] after:ml-[0.25rem] after:mt-[0.25rem] hover:cursor-pointer col-start-2 row-start-1 peer" class="mr-2 w-9 h-5 ml-2 rounded-full appearance-none mt-[0.3rem] bg-marine-blue after:absolute after:h-3 after:w-3 after:rounded-full after:border-none after:bg-neutral-100 after:transition-[background-color_0.2s,transform_0.2s] checked:after:ml-[1.2rem] after:ml-[0.25rem] after:mt-[0.25rem] hover:cursor-pointer col-start-2 row-start-1 peer md:mx-4"
type="checkbox" type="checkbox"
name="isPackageYearly"
role="switch" role="switch"
id="packageDuration" id="packageDuration"
th:checked="${formData.userAnswers.step2.isYearly}"
hx-get="/step2/planTypeInputs"
hx-target="#plan-type-inputs"
hx-swap="outerHTML"
/> />
<span class="row-start-1 text-marine-blue peer-checked:text-cool-gray">Monthly</span> <p
<span class="row-start-1 text-cool-gray peer-checked:text-marine-blue">Yearly</span> class="row-start-1 text-marine-blue peer-checked:text-cool-gray"
>
Monthly
</p>
<p
class="row-start-1 text-cool-gray peer-checked:text-marine-blue"
>
Yearly
</p>
</div> </div>
</div> </div>
@ -177,18 +232,27 @@
class="flex flex-row items-center py-4 w-full bg-white md:items-end md:h-full" class="flex flex-row items-center py-4 w-full bg-white md:items-end md:h-full"
> >
<a <a
hx-post="/submit-step/2/1"
hx-swap="outerHTML"
hx-target="#form-step"
href="step1.html" href="step1.html"
class="ml-6 text-sm font-semibold md:pb-3 md:ml-20 md:text-base text-cool-gray" class="ml-6 text-sm font-semibold md:pb-3 md:ml-24 md:text-base text-cool-gray"
>Go Back</a >Go Back</a
> >
<div class="grow"></div> <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 <a
th:remove="all"
href="step3.html" 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" 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 >Next Step</a
> >
</section> </section>
</article> </form>
</main> </main>
</body> </body>
</html> </html>

View File

@ -36,13 +36,19 @@
<![endif]--> <![endif]-->
<main class="grid place-content-center h-screen"> <main class="grid place-content-center h-screen">
<article <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" 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" id="form-step"
hx-post="/submit-step/3/4"
hx-swap="outerHTML"
action="/submit-step/3/4"
method="post"
th:fragment="formFragment(formData)"
> >
<summary <summary
class="w-full h-44 bg-no-repeat md:row-span-2 bg-sidebar-mobile marker:text-white md:bg-sidebar-desktop md:h-[568px] md:w-[274px]" class="w-full h-44 bg-no-repeat md:row-span-2 bg-sidebar-mobile marker:text-white md:bg-sidebar-desktop md:h-[568px] md:w-[274px]"
id="sidebar" id="sidebar"
th:replace="~{step1::stepsSummary (${formData})}"
> >
<ol <ol
class="grid grid-cols-[repeat(4,_auto)] gap-x-5 content-center items-center place-content-center h-24 md:flex-col md:h-full md:grid-rows-[repeat(4,_auto)] md:grid-cols-1 md:content-start md:p-10 md:gap-y-7 text-white text-sm uppercase" class="grid grid-cols-[repeat(4,_auto)] gap-x-5 content-center items-center place-content-center h-24 md:flex-col md:h-full md:grid-rows-[repeat(4,_auto)] md:grid-cols-1 md:content-start md:p-10 md:gap-y-7 text-white text-sm uppercase"
@ -101,62 +107,100 @@
<h1 class="text-2xl font-bold md:text-4xl text-marine-blue"> <h1 class="text-2xl font-bold md:text-4xl text-marine-blue">
Pick add-ons Pick add-ons
</h1> </h1>
<p class="py-3 text-cool-gray"> <p class="py-3 md:pb-10 text-cool-gray">
Add-ons help enhance your gaming experience. Add-ons help enhance your gaming experience.
</p> </p>
<div <div class="flex flex-col gap-y-3 w-full text-sm md:text-base">
class="flex flex-col w-full text-sm md:text-base" <label
th:each="addon: ${formData.availableAddons}"
for="multiplayer-games"
th:for="${addon}"
class="relative pl-5 h-16 md:w-full md:h-20"
> >
<label for="multiplayer-games" class="relative pl-6 h-20 md:w-full">
<input <input
id="multiplayer-games" id="multiplayer-games"
th:id="${addon}"
type="checkbox" type="checkbox"
value="OnlineService"
th:value="${addon}"
name="addon-services" name="addon-services"
class="my-7 w-6 h-6 peer" class="absolute z-40 my-5 w-6 h-6 text-white rounded-lg border md:my-7 accent-purplish-blue border-light-gray peer"
th:checked="${formData.userAnswers.step3.containsAddon(addon)}"
/> />
<div <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" class="absolute inset-y-0 inset-x-0 z-20 rounded-lg border border-cool-gray peer-checked:border-purplish-blue peer-checked:bg-magnolia/75 hover:bg-magnolia/[.1] hover:border-purplish-blue"
> >
<div class="grid place-content-center ml-20 h-full grid-cols-[1fr_100px]"> <div
<h1>Online Service</h1> class="grid place-content-center ml-16 h-full grid-cols-[1fr_70px]"
<p>Access to multiplayer games</p> >
<p class="col-start-2 row-span-2 row-start-1 self-center">+$1/mo</p> <h1 th:text="${addon.name}"
class="font-bold text-marine-blue"
>Online Service</h1>
<p th:text="${addon.description}"
class="text-xs text-cool-gray"
>
Access to multiplayer games
</p>
<p
class="col-start-2 row-span-2 row-start-1 self-center text-purplish-blue"
th:text="|+$${formData.addonCost(addon)}/${formData.periodCostLabel}|"
>
+$1/mo
</p>
</div> </div>
</div> </div>
</label> </label>
<label
<label for="larger-storage" class="relative pl-6 h-20 md:w-full"> for="larger-storage"
class="relative pl-6 h-20 md:w-full"
th:remove="all"
>
<input <input
id="larger-storage" id="larger-storage"
type="checkbox" type="checkbox"
name="addon-services" name="addon-services"
value="LargerStorage"
class="my-7 w-6 h-6 peer" class="my-7 w-6 h-6 peer"
th:checked="${formData.userAnswers.step3.containsAddon('LargerStorage')}"
/> />
<div <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" 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> <h1>Larger storage</h1>
<p>Extra 1TB of cloud save</p> <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>
</div> </div>
</label> </label>
<label
<label for="custom-profile" class="relative pl-6 h-20 md:w-full"> for="custom-profile"
class="relative pl-6 h-20 md:w-full"
th:remove="all"
>
<input <input
id="custom-profile" id="custom-profile"
type="checkbox" type="checkbox"
name="addon-services" name="addon-services"
value="CustomProfile"
class="my-7 w-6 h-6 peer" class="my-7 w-6 h-6 peer"
th:checked="${formData.userAnswers.step3.containsAddon('CustomProfile')}"
/> />
<div <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" 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> <h1>Customizable Profile</h1>
<p>Custom theme on your profile</p> <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>
</div> </div>
</label> </label>
@ -170,18 +214,28 @@
class="flex flex-row items-center py-4 w-full bg-white md:items-end md:h-full" class="flex flex-row items-center py-4 w-full bg-white md:items-end md:h-full"
> >
<a <a
hx-post="/submit-step/3/2"
hx-swap="outerHTML"
hx-target="#form-step"
href="step2.html" href="step2.html"
class="ml-6 text-sm font-semibold md:pb-3 md:ml-20 md:text-base text-cool-gray" class="ml-6 text-sm font-semibold md:pb-3 md:ml-24 md:text-base text-cool-gray"
>Go Back</a >Go Back</a
> >
<div class="grow"></div> <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 <a
th:remove="all"
href="step4.html" 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" 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 >Next Step</a
> >
</section> </section>
</article> </form>
</main> </main>
</body> </body>
</html> </html>

View File

@ -36,13 +36,19 @@
<![endif]--> <![endif]-->
<main class="grid place-content-center h-screen"> <main class="grid place-content-center h-screen">
<article <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" 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" id="form-step"
hx-post="/submit-step/4/5"
action="/submit-step/4/5"
hx-swap="outerHTML"
method="post"
th:fragment="formFragment(formData)"
> >
<summary <summary
class="w-full h-44 bg-no-repeat md:row-span-2 bg-sidebar-mobile marker:text-white md:bg-sidebar-desktop md:h-[568px] md:w-[274px]" class="w-full h-44 bg-no-repeat md:row-span-2 bg-sidebar-mobile marker:text-white md:bg-sidebar-desktop md:h-[568px] md:w-[274px]"
id="sidebar" id="sidebar"
th:replace="~{step1::stepsSummary (${formData})}"
> >
<ol <ol
class="grid grid-cols-[repeat(4,_auto)] gap-x-5 content-center items-center place-content-center h-24 md:flex-col md:h-full md:grid-rows-[repeat(4,_auto)] md:grid-cols-1 md:content-start md:p-10 md:gap-y-7 text-white text-sm uppercase" class="grid grid-cols-[repeat(4,_auto)] gap-x-5 content-center items-center place-content-center h-24 md:flex-col md:h-full md:grid-rows-[repeat(4,_auto)] md:grid-cols-1 md:content-start md:p-10 md:gap-y-7 text-white text-sm uppercase"
@ -98,28 +104,69 @@
class="flex flex-col py-8 px-6 -mt-20 w-11/12 bg-white rounded-xl md:px-24 md:mt-0 md:w-full drop-shadow-xl md:drop-shadow-none" class="flex flex-col py-8 px-6 -mt-20 w-11/12 bg-white rounded-xl md:px-24 md:mt-0 md:w-full drop-shadow-xl md:drop-shadow-none"
> >
<!-- Step 3 start --> <!-- Step 3 start -->
<h1 class="text-2xl font-bold md:text-4xl text-marine-blue">Finishing up</h1> <h1 class="text-2xl font-bold md:text-4xl text-marine-blue">
<p class="py-3 text-cool-gray">Double-check everything looks OK before confirming.</p> Finishing up
</h1>
<p class="pt-3 pb-5 text-cool-gray">
Double-check everything looks OK before confirming.
</p>
<div <div
class="flex flex-col w-full text-sm rounded-lg divide-y md:text-base bg-magnolia" class="flex flex-col px-3 w-full text-sm rounded-lg divide-y md:text-base bg-magnolia/75"
id="selection-overview" id="selection-overview"
> >
<div id="selected-plan"> <div id="selected-plan" class="grid py-3 grid-cols-[1fr_auto]">
Arcade (Monthly) Change $90/y <h2 th:text="${formData.fullPlanName}"
</div> class="font-bold text-marine-blue"
<div id="selected-addons" >Arcade (Monthly)</h2>
class="flex flex-col" <p
th:text="|$${formData.selectedPlanCost}/${formData.periodCostLabel}|"
class="row-span-2 self-end font-bold text-marine-blue"
>
$90/y
</p>
<a
hx-post="/submit-step/4/2"
hx-swap="outerHTML"
hx-target="#form-step"
href="step3.html"
hx-params="not form-confirmed"
class="underline text-cool-gray"
>Change</a
> >
<div>
Online service +$10/yr
</div> </div>
<div> <div id="selected-addons" class="flex flex-col gap-y-4 py-3 mp-10"
Larger storage +$20/yr th:if="not ${formData.userAnswers.step3.addonsAsJava.isEmpty}"
>
<div
th:each="addon: ${formData.userAnswers.step3.addonsAsJava}"
class="flex flex-row"
>
<p th:text="${addon.name}" class="grow text-cool-gray">Online service</p>
<p
th:text="|+$${formData.addonCost(addon)}/${formData.periodCostLabel}|"
>
+$10/yr
</p>
</div> </div>
<div th:remove="all">Larger storage +$20/yr</div>
</div> </div>
</div> </div>
<p>Total (per year) $120</p> <section class="flex flex-row p-3 mt-5">
<p class="grow text-cool-gray">
Total (per
<span
th:text="${formData.userAnswers.step2.isYearly} ? 'year' : 'month'"
>year</span
>)
</p>
<p
th:text="|+$${formData.fullOrderPrice}/${formData.periodCostLabel}|"
class="font-bold text-purplish-blue"
>
$120
</p>
</section>
<!-- Step 3 end --> <!-- Step 3 end -->
</section> </section>
@ -129,18 +176,29 @@
class="flex flex-row items-center py-4 w-full bg-white md:items-end md:h-full" class="flex flex-row items-center py-4 w-full bg-white md:items-end md:h-full"
> >
<a <a
hx-post="/submit-step/4/3"
hx-swap="outerHTML"
hx-target="#form-step"
href="step3.html" href="step3.html"
class="ml-6 text-sm font-semibold md:pb-3 md:ml-20 md:text-base text-cool-gray" class="ml-6 text-sm font-semibold md:pb-3 md:ml-20 md:text-base text-cool-gray"
hx-params="not form-confirmed"
>Go Back</a >Go Back</a
> >
<div class="grow"></div> <div class="grow"></div>
<input type="hidden" name="form-confirmed" value="true" />
<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 <a
th:remove="all"
href="step5.html" 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" 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 >Confirm</a
> >
</section> </section>
</article> </form>
</main> </main>
</body> </body>
</html> </html>

View File

@ -35,16 +35,85 @@
</p> </p>
<![endif]--> <![endif]-->
<main class="bg-green-200"> <main class="grid place-content-center h-screen">
<article
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"
th:fragment="formFragment(formData)"
>
<summary
class="w-full h-44 bg-no-repeat md:row-span-2 bg-sidebar-mobile marker:text-white md:bg-sidebar-desktop md:h-[568px] md:w-[274px]"
id="sidebar"
th:replace="~{step1::stepsSummary (${formData})}"
>
<ol
class="grid grid-cols-[repeat(4,_auto)] gap-x-5 content-center items-center place-content-center h-24 md:flex-col md:h-full md:grid-rows-[repeat(4,_auto)] md:grid-cols-1 md:content-start md:p-10 md:gap-y-7 text-white text-sm uppercase"
>
<li class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]">
<div
class="grid place-content-center w-8 h-8 text-white rounded-full border border-white"
>
1
</div>
<p class="hidden md:flex md:flex-col">
<span class="text-light-gray">Step 1</span
><span class="font-bold">Your info</span>
</p>
</li>
<li class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]">
<div
class="grid place-content-center w-8 h-8 text-white rounded-full border border-white"
>
2
</div>
<p class="hidden md:flex md:flex-col">
<span class="text-light-gray">Step 2</span
><span class="font-bold">Select plan</span>
</p>
</li>
<li class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]">
<div
class="grid place-content-center w-8 h-8 text-white rounded-full border border-white"
>
3
</div>
<p class="hidden md:flex md:flex-col">
<span class="text-light-gray">Step 3</span
><span class="font-bold">Add-ons</span>
</p>
</li>
<li class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]">
<div
class="grid place-content-center w-8 h-8 text-white rounded-full border border-white"
>
4
</div>
<p class="hidden md:flex md:flex-col">
<span class="text-light-gray">Step 4</span
><span class="font-bold">summary</span>
</p>
</li>
</ol>
</summary>
<section
id="multipage-form-container"
class="flex flex-col gap-y-4 items-center py-8 py-20 px-6 -mt-20 w-11/12 bg-white rounded-xl md:px-24 md:mt-0 md:w-full drop-shadow-xl md:drop-shadow-none"
>
<!-- Step 5 start --> <!-- Step 5 start -->
Thank you! <img src="../public/images/icon-thank-you.svg" alt="" class="mb-2 w-14 h-14" />
<h2 class="text-2xl font-bold">Thank you!</h2>
<p class="text-center text-cool-gray">
Thanks for confirming your subscription! We hope you have fun Thanks for confirming your subscription! We hope you have fun
using our platform. If you ever need support, please feel free using our platform. If you ever need support, please feel free
to email us at support@loremgaming.com. to email us at support@loremgaming.com.
</p>
<!-- Step 5 end --> <!-- Step 5 end -->
</section>
<div class="md:hidden grow"></div>
</article>
</main> </main>
</body> </body>
</html> </html>

View File

@ -1,10 +1,221 @@
package multistepform package multistepform
import java.util.UUID import java.util.UUID
import scala.jdk.CollectionConverters._
object Models { object Models {
/** Labels and form info which dynamically depend on user answers e.g plan
* name 'Arcade (Yearly)' vs 'Pro (Monthly)'
*/
final case class FormData(
userAnswers: Answers
) {
val stepsAmount = Steps.values.toList.asJava
// yeah, in real world it will not be this simple
def yearlyCost(monthlyCost: Int): Int = 10 * monthlyCost
def periodCostLabel: String = {
if (userAnswers.step2.isYearly) "yr" else "mo"
}
def selectedPlanCost: Int = planCost(userAnswers.step2.planType)
def planCost(plan: PlanType): Int = {
val monthlyPlanCost = plan.monthlyCost
if (userAnswers.step2.isYearly) yearlyCost(monthlyPlanCost)
else monthlyPlanCost
}
def addonCost(addon: Addons): Int = {
val monthCost = addon.monthlyCost
if (userAnswers.step2.isYearly) yearlyCost(monthCost) else monthCost
}
def fullPlanName: String = {
val period = if (userAnswers.step2.isYearly) "Yearly" else "Monthly"
s"${userAnswers.step2.planType} (${period})"
}
def fullOrderPrice: Int = {
selectedPlanCost + userAnswers.step3.addons.toList.map(addonCost).sum
}
def availablePlans = PlanType.values.toList.asJava
def availableAddons = Addons.values.toList.asJava
}
final case class Answers( final case class Answers(
sessionId: String = "id1", sessionId: String = "",
currentStep: Int = 1, currentStep: Int = 4,
step1: StepAnswers.Step1 = StepAnswers.Step1(),
step2: StepAnswers.Step2 = StepAnswers.Step2(),
step3: StepAnswers.Step3 = StepAnswers.Step3(
addons = Addons.values.toSet
),
step4: StepAnswers.Step4 = StepAnswers.Step4()
) {
// 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, nextStep: Int): Answers = {
stepNum match {
case 1 =>
this.copy(
step1 = this.step1.fromFormData(rawData),
currentStep = nextStep
) )
case 2 =>
this.copy(
step2 = this.step2.fromFormData(rawData),
currentStep = nextStep
)
case 3 =>
this.copy(
step3 = this.step3.fromFormData(rawData),
currentStep = nextStep
)
case 4 =>
this.copy(
step4 = this.step4.fromFormData(rawData),
currentStep = nextStep
)
case _ => this
}
}
}
/** TODO would be nice to connect answers to the steps enum in some helpful
* way.
*/
enum Steps(val index: Int, val name: String):
case Step1 extends Steps(1, "Your info")
case Step2 extends Steps(2, "Select plan")
case Step3 extends Steps(3, "Add-ons")
case Step4 extends Steps(4, "Summary")
enum PlanType(val monthlyCost: Int, val iconPath: String):
case Arcade extends PlanType(9, "public/images/icon-arcade.svg")
case Advanced extends PlanType(12, "public/images/icon-advanced.svg")
case Pro extends PlanType(15, "public/images/icon-pro.svg")
def name(): String = {
this.toString().replaceAll("([a-z])([A-Z])", "$1 $2")
}
enum Addons(val monthlyCost: Int, val description: String):
case OnlineService extends Addons(1, "Access to multiplayer games")
case LargerStorage extends Addons(2, "Extra 1TB of cloud storage")
case CustomProfile extends Addons(2, "Custom theme on your profile")
/** Change camel case into human readable. Adding single space before each
* uppercase
*/
def name(): String = {
this.toString().replaceAll("([a-z])([A-Z])", "$1 $2")
}
sealed trait StepAnswers {
def fromFormData(rawData: String): StepAnswers
def submitted: Boolean
}
object StepAnswers {
final case class Step1(
name: String = "",
email: String = "",
phone: String = "",
override val submitted: Boolean = false
) extends StepAnswers {
override def fromFormData(rawData: String): Step1 = {
println(s"parsing step 1 data $rawData")
val fieldValues = rawData
.split("&")
.filterNot(_.isEmpty())
.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, submitted = true)
}
}
final case class Step2(
planType: PlanType = PlanType.Arcade,
isYearly: Boolean = false,
override val submitted: Boolean = false
) extends StepAnswers {
override def fromFormData(rawData: String): Step2 = {
println(s"parsing step 2 data $rawData")
val fieldValues = rawData
.split("&")
.filterNot(_.isEmpty())
.map { field =>
val Array(name, value) = field.split("=")
name -> value
}
.toMap
val planType =
PlanType.valueOf(fieldValues.getOrElse("plan-type", "Arcade"))
val isYearly = fieldValues.get("isPackageYearly").contains("on")
Step2(planType, isYearly, submitted = true)
}
}
final case class Step3(
addons: Set[Addons] = Set.empty,
override val submitted: Boolean = false
) extends StepAnswers {
def addonsAsJava = addons.asJava
def containsAddon(addonName: String): Boolean = {
addons.contains(Addons.valueOf(addonName))
}
override def fromFormData(rawData: String): Step3 = {
println(s"parsing step 3 data $rawData")
// for multiple checkboxes data comes in form of
// addon-services=OnlineService&addon-services=CustomProfile
val fieldValues = rawData
.split("&")
.filterNot(_.isEmpty())
.map { field =>
val Array(name, value) = field.split("=")
name -> value
}
val addonsStrings = fieldValues
.groupMap(_._1)(_._2)
.getOrElse("addon-services", Array.empty[String])
println(s"in step 3 got strings ${addonsStrings.mkString(", ")}")
val addons = addonsStrings.map(Addons.valueOf(_)).toSet
Step3(addons, submitted = true)
}
}
final case class Step4(
override val submitted: Boolean = false
) extends StepAnswers {
override def fromFormData(rawData: String): Step4 = {
val fieldValues = rawData
.split("&")
.filterNot(_.isEmpty())
.map { field =>
println(s"working with field $field")
val Array(name, value) = field.split("=")
name -> value
}
.toMap
val isConfirmed = fieldValues.contains("form-confirmed")
Step4(isConfirmed)
}
}
}
} }

View File

@ -3,6 +3,12 @@ package multistepform
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
import org.thymeleaf.TemplateEngine import org.thymeleaf.TemplateEngine
import org.thymeleaf.context.Context 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) case class Routes()(implicit cc: castor.Context, log: cask.Logger)
extends cask.Routes { extends cask.Routes {
@ -14,16 +20,144 @@ case class Routes()(implicit cc: castor.Context, log: cask.Logger)
val templateEngine = new TemplateEngine() val templateEngine = new TemplateEngine()
templateEngine.setTemplateResolver(templateResolver) 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("/") @cask.get("/")
def getIndex() = { 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 context = new Context()
val indexPage = templateEngine.process("index", context) val indexPage = templateEngine.process("index", context)
cask.Response( cask.Response(
indexPage, 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") 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") @cask.staticResources("/public")
def publicFiles() = "public" def publicFiles() = "public"

View File

@ -0,0 +1,11 @@
package multistepform
import multistepform.Models.Answers
object Sessions {
// the simplest form of storing data
// i'll be relying on Render.com killing my app after 15 minutes of inactivity for GC
// no need to manage concurrency really, because requests for same session are really far apart
// and load will average to be 10req/day :shrug:
val sessionReplies = scala.collection.mutable.Map.empty[String, Answers]
}