Compare commits

..

No commits in common. "0e7d53b5829be738f6765cef4bc1c3d96ead987c" and "3c51273cc38e85ab221d1f69af87e5cebd14165f" have entirely different histories.

13 changed files with 180 additions and 1252 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,32 +0,0 @@
<!-- 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>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

View File

@ -548,26 +548,30 @@ video {
bottom: 0px; bottom: 0px;
} }
.end-0 { .z-0 {
inset-inline-end: 0px; z-index: 0;
} }
.top-0 { .z-10 {
top: 0px; z-index: 10;
}
.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;
} }
@ -576,8 +580,26 @@ video {
grid-row-start: 1; grid-row-start: 1;
} }
.m-3 { .m-8 {
margin: 0.75rem; margin: 2rem;
}
.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 {
@ -585,47 +607,14 @@ 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;
} }
@ -642,36 +631,20 @@ video {
margin-top: 0.3rem; margin-top: 0.3rem;
} }
.mt-3 { .ml-\[200px\] {
margin-top: 0.75rem; margin-left: 200px;
} }
.mb-4 { .ml-\[100px\] {
margin-bottom: 1rem; margin-left: 100px;
} }
.mt-2 { .ml-\[70px\] {
margin-top: 0.5rem; margin-left: 70px;
} }
.ml-16 { .ml-20 {
margin-left: 4rem; margin-left: 5rem;
}
.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 {
@ -698,10 +671,6 @@ video {
height: 3rem; height: 3rem;
} }
.h-14 {
height: 3.5rem;
}
.h-20 { .h-20 {
height: 5rem; height: 5rem;
} }
@ -718,42 +687,34 @@ video {
height: 1.25rem; height: 1.25rem;
} }
.h-6 {
height: 1.5rem;
}
.h-8 { .h-8 {
height: 2rem; height: 2rem;
} }
.h-full {
height: 100%;
}
.h-screen { .h-screen {
height: 100vh; height: 100vh;
} }
.h-16 { .h-full {
height: 4rem; height: 100%;
}
.h-6 {
height: 1.5rem;
}
.h-4 {
height: 1rem;
} }
.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;
} }
@ -770,6 +731,14 @@ video {
width: 100vw; width: 100vw;
} }
.w-6 {
width: 1.5rem;
}
.w-4 {
width: 1rem;
}
.grow { .grow {
flex-grow: 1; flex-grow: 1;
} }
@ -788,28 +757,12 @@ 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_auto\] { .grid-cols-\[1fr_100px\] {
grid-template-columns: 1fr auto; grid-template-columns: 1fr 100px;
}
.grid-cols-\[1fr_50px\] {
grid-template-columns: 1fr 50px;
}
.grid-cols-\[1fr_70px\] {
grid-template-columns: 1fr 70px;
} }
.flex-row { .flex-row {
@ -836,10 +789,6 @@ 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;
@ -849,28 +798,20 @@ 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));
} }
.place-self-center {
place-self: center;
}
.self-end {
align-self: flex-end;
}
.self-center { .self-center {
align-self: center; align-self: center;
} }
.justify-self-center {
justify-self: center;
}
.rounded { .rounded {
border-radius: 0.25rem; border-radius: 0.25rem;
} }
@ -901,16 +842,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));
@ -921,76 +862,48 @@ 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-red-500 { .bg-yellow-500 {
--tw-bg-opacity: 1 !important;
background-color: rgb(239 68 68 / var(--tw-bg-opacity)) !important;
}
.\!bg-white {
--tw-bg-opacity: 1 !important;
background-color: rgb(255 255 255 / var(--tw-bg-opacity)) !important;
}
.\!bg-light-blue {
--tw-bg-opacity: 1 !important;
background-color: hsl(206 94% 87% / var(--tw-bg-opacity)) !important;
}
.bg-light-gray {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: hsl(229 24% 87% / var(--tw-bg-opacity)); background-color: rgb(234 179 8 / var(--tw-bg-opacity));
} }
.bg-light-gray\/50 { .bg-cool-gray {
background-color: hsl(229 24% 87% / 0.5); --tw-bg-opacity: 1;
background-color: hsl(231 11% 63% / var(--tw-bg-opacity));
} }
.bg-light-gray\/25 { .bg-light-blue {
background-color: hsl(229 24% 87% / 0.25); --tw-bg-opacity: 1;
background-color: hsl(206 94% 87% / var(--tw-bg-opacity));
} }
.bg-magnolia\/75 { .bg-pastel-blue {
background-color: hsl(217 100% 97% / 0.75); --tw-bg-opacity: 1;
background-color: hsl(228 100% 84% / var(--tw-bg-opacity));
}
.bg-purplish-blue {
--tw-bg-opacity: 1;
background-color: hsl(243 100% 62% / var(--tw-bg-opacity));
} }
.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;
@ -1001,11 +914,6 @@ 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;
@ -1021,33 +929,20 @@ 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-4 { .pl-8 {
padding-left: 1rem; padding-left: 2rem;
} }
.pl-5 { .pl-5 {
padding-left: 1.25rem; padding-left: 1.25rem;
} }
.pb-5 { .pl-6 {
padding-bottom: 1.25rem; padding-left: 1.5rem;
}
.text-center {
text-align: center;
} }
.text-2xl { .text-2xl {
@ -1065,11 +960,6 @@ 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;
} }
@ -1087,11 +977,6 @@ 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));
@ -1107,50 +992,6 @@ 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);
@ -1270,105 +1111,15 @@ 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));
@ -1383,16 +1134,8 @@ html {
background-color: hsl(217 100% 97% / 0.5); background-color: hsl(217 100% 97% / 0.5);
} }
.peer:checked ~ .peer-checked\:bg-magnolia\/75 { .peer:checked ~ .peer-checked\:bg-magnolia\/20 {
background-color: hsl(217 100% 97% / 0.75); background-color: hsl(217 100% 97% / 0.2);
}
.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 {
@ -1410,30 +1153,6 @@ 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;
} }
@ -1446,22 +1165,6 @@ 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;
} }
@ -1490,30 +1193,6 @@ 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;
} }
@ -1530,8 +1209,8 @@ html {
width: 100%; width: 100%;
} }
.md\:grow { .md\:w-20 {
flex-grow: 1; width: 5rem;
} }
.md\:grid-cols-1 { .md\:grid-cols-1 {
@ -1546,10 +1225,6 @@ 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;
} }
@ -1570,10 +1245,6 @@ 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;
@ -1583,14 +1254,6 @@ 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;
} }
@ -1620,10 +1283,6 @@ 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;
@ -1634,16 +1293,6 @@ 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;
} }
@ -1656,10 +1305,6 @@ 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;
@ -1670,21 +1315,6 @@ 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,15 +9,10 @@
rel="icon" rel="icon"
type="image/png" type="image/png"
sizes="32x32" sizes="32x32"
th:href="'public/images/favicon-32x32.png'" href="./assets/images/favicon-32x32.png"
href="../public/images/favicon-32x32.png"
/> />
<link <link href="../public/out.css" rel="stylesheet" />
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 👍 -->
@ -32,25 +27,7 @@
</style> </style>
</head> </head>
<body> <body>
<main class="grid place-content-center w-screen h-screen bg-magnolia"> <main class="bg-light-gray h-screen w-screen">
<section <!-- here be immediate hx-get for the form. to subscitute the body -->
hx-get="/get-form"
hx-trigger="load"
hx-swap="outerHTML"
>
<!-- 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,82 +35,55 @@
</p> </p>
<![endif]--> <![endif]-->
<form <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" 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 bg-cover 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: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 <li class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]">
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 font-bold text-white rounded-full border border-white" class="grid place-content-center w-8 h-8 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" <span class="text-light-gray">Step 1</span><span class="font-bold">Your info</span>
>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 <li class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]">
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="text-light-gray">Step 2</span><span class="font-bold">Select plan</span>
><span class="font-bold">Select plan</span>
</p> </p>
</li> </li>
<li <li class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]">
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="text-light-gray">Step 3</span><span class="font-bold">Add-ons</span>
><span class="font-bold">Add-ons</span>
</p> </p>
</li> </li>
<li <li class="items-center md:grid md:gap-x-4 md:grid-cols-[auto_1fr]">
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="text-light-gray">Step 4</span><span class="font-bold">summary</span>
><span class="font-bold">summary</span>
</p> </p>
</li> </li>
</ol> </ol>
@ -120,47 +93,34 @@
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"> <h1 class="text-2xl font-bold md:text-4xl text-marine-blue">Personal info</h1>
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" <label for="name" class="pt-3 text-sm md:pt-5 md:pb-2 text-marine-blue">Name</label>
>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 focus:outline-none placeholder:text-cool-gray invalid:border-strawberry-red focus:border-marine-blue" 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"
/> />
<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 focus:outline-none placeholder:text-cool-gray invalid:border-strawberry-red focus:border-marine-blue" 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"
/> />
<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 focus:outline-none placeholder:text-cool-gray invalid:border-strawberry-red focus:border-marine-blue" 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"
/> />
<!-- Step 1 end --> <!-- Step 1 end -->
@ -171,19 +131,13 @@
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>
</form> </article>
<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,19 +36,13 @@
<![endif]--> <![endif]-->
<main class="grid place-content-center h-screen"> <main class="grid place-content-center h-screen">
<form <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" 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"
@ -107,120 +101,71 @@
<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 mb-4 text-cool-gray"> <p class="py-3 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 gap-y-3 my-3 w-full md:flex-row md:gap-x-4" class="flex flex-col w-full md:flex-row"
id="plan-type-inputs"
th:fragment="planTypesInputs(formData)"
> >
<label <label for="ArcadePlanType" class="relative h-20 md:w-32">
for="ArcadePlanType"
class="relative h-20 md:h-40 md:grow"
th:each="planType: ${formData.availablePlans}"
th:for="${planType}"
>
<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="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" class="absolute inset-y-0 inset-x-0 rounded-lg border border-cool-gray peer-checked:border-purplish-blue peer-checked:bg-magnolia"
> >
<img <span class="">Arcade</span>
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 <label for="AdvancedPlanType" class="relative h-20 md:w-32">
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"
> >
<img src="../public/images/icon-advanced.svg" alt="" /> <span>Advanced</span>
<h2>Advanced</h2>
</div> </div>
</label> </label>
<label <label for="ProPlanType" class="relative h-20 md:w-32">
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"
> >
<img src="../public/images/icon-pro.svg" alt="" /> <span>Pro</span>
<h2>Pro</h2>
</div> </div>
</label> </label>
</div> </div>
<div <div class="grid grid-flow-col-dense place-content-center w-full rounded-lg bg-magnolia">
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 md:mx-4" 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"
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"
/> />
<p <span class="row-start-1 text-marine-blue peer-checked:text-cool-gray">Monthly</span>
class="row-start-1 text-marine-blue peer-checked:text-cool-gray" <span class="row-start-1 text-cool-gray peer-checked:text-marine-blue">Yearly</span>
>
Monthly
</p>
<p
class="row-start-1 text-cool-gray peer-checked:text-marine-blue"
>
Yearly
</p>
</div> </div>
</div> </div>
@ -232,27 +177,18 @@
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-24 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"
>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>
</form> </article>
</main> </main>
</body> </body>
</html> </html>

View File

@ -36,19 +36,13 @@
<![endif]--> <![endif]-->
<main class="grid place-content-center h-screen"> <main class="grid place-content-center h-screen">
<form <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" 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"
@ -107,100 +101,62 @@
<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 md:pb-10 text-cool-gray"> <p class="py-3 text-cool-gray">
Add-ons help enhance your gaming experience. Add-ons help enhance your gaming experience.
</p> </p>
<div class="flex flex-col gap-y-3 w-full text-sm md:text-base"> <div
<label class="flex flex-col w-full text-sm md:text-base"
th:each="addon: ${formData.availableAddons}" >
for="multiplayer-games" <label for="multiplayer-games" class="relative pl-6 h-20 md:w-full">
th:for="${addon}"
class="relative pl-5 h-16 md:w-full md:h-20"
>
<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="absolute z-40 my-5 w-6 h-6 text-white rounded-lg border md:my-7 accent-purplish-blue border-light-gray peer" class="my-7 w-6 h-6 peer"
th:checked="${formData.userAnswers.step3.containsAddon(addon)}"
/> />
<div <div
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" 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 <div class="grid place-content-center ml-20 h-full grid-cols-[1fr_100px]">
class="grid place-content-center ml-16 h-full grid-cols-[1fr_70px]" <h1>Online Service</h1>
> <p>Access to multiplayer games</p>
<h1 th:text="${addon.name}" <p class="col-start-2 row-span-2 row-start-1 self-center">+$1/mo</p>
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
for="larger-storage" <label for="larger-storage" class="relative pl-6 h-20 md:w-full">
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 <div class="grid place-content-center ml-20 h-full grid-cols-[1fr_100px]">
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"> <p class="col-start-2 row-span-2 row-start-1 self-center">+$2/mo</p>
+$2/mo
</p>
</div> </div>
</div> </div>
</label> </label>
<label
for="custom-profile" <label for="custom-profile" class="relative pl-6 h-20 md:w-full">
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 <div class="grid place-content-center ml-20 h-full grid-cols-[1fr_100px]">
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"> <p class="col-start-2 row-span-2 row-start-1 self-center">+$2/mo</p>
+$2/mo
</p>
</div> </div>
</div> </div>
</label> </label>
@ -214,28 +170,18 @@
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-24 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"
>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>
</form> </article>
</main> </main>
</body> </body>
</html> </html>

View File

@ -36,19 +36,13 @@
<![endif]--> <![endif]-->
<main class="grid place-content-center h-screen"> <main class="grid place-content-center h-screen">
<form <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" 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"
@ -104,69 +98,28 @@
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"> <h1 class="text-2xl font-bold md:text-4xl text-marine-blue">Finishing up</h1>
Finishing up <p class="py-3 text-cool-gray">Double-check everything looks OK before confirming.</p>
</h1>
<p class="pt-3 pb-5 text-cool-gray">
Double-check everything looks OK before confirming.
</p>
<div <div
class="flex flex-col px-3 w-full text-sm rounded-lg divide-y md:text-base bg-magnolia/75" class="flex flex-col w-full text-sm rounded-lg divide-y md:text-base bg-magnolia"
id="selection-overview" id="selection-overview"
> >
<div id="selected-plan" class="grid py-3 grid-cols-[1fr_auto]"> <div id="selected-plan">
<h2 th:text="${formData.fullPlanName}" Arcade (Monthly) Change $90/y
class="font-bold text-marine-blue"
>Arcade (Monthly)</h2>
<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> </div>
<div id="selected-addons" class="flex flex-col gap-y-4 py-3 mp-10" <div id="selected-addons"
th:if="not ${formData.userAnswers.step3.addonsAsJava.isEmpty}" class="flex flex-col"
> >
<div <div>
th:each="addon: ${formData.userAnswers.step3.addonsAsJava}" Online service +$10/yr
class="flex flex-row" </div>
> <div>
<p th:text="${addon.name}" class="grow text-cool-gray">Online service</p> Larger storage +$20/yr
<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>
<section class="flex flex-row p-3 mt-5"> <p>Total (per year) $120</p>
<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>
@ -176,29 +129,18 @@
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>
</form> </article>
</main> </main>
</body> </body>
</html> </html>

View File

@ -35,85 +35,16 @@
</p> </p>
<![endif]--> <![endif]-->
<main class="grid place-content-center h-screen"> <main class="bg-green-200">
<article <!-- Step 5 start -->
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 -->
<img src="../public/images/icon-thank-you.svg" alt="" class="mb-2 w-14 h-14" /> Thank you!
<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,221 +1,10 @@
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 = "", sessionId: String = "id1",
currentStep: Int = 4, currentStep: Int = 1,
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,12 +3,6 @@ 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 {
@ -20,144 +14,16 @@ 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(ctx: cask.Request) = { def getIndex() = {
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

@ -1,11 +0,0 @@
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]
}