() {
21 |
22 | private val presenter = PresenterProvider()
23 |
24 | override fun AppState.init() {
25 | activeView = ActiveView.UserInput
26 | premium = 0
27 | }
28 |
29 | override fun RBuilder.render() {
30 | when (state.activeView) {
31 | ActiveView.UserInput -> userInput(presenter, state.user, ::showResult)
32 | ActiveView.Result -> displayResults(presenter, state.premium, ::showInput)
33 | }
34 | }
35 |
36 | private fun showResult(user: User, result: Int) {
37 | setState {
38 | this.user = user
39 | this.premium = result
40 | this.activeView = ActiveView.Result
41 | }
42 | }
43 |
44 | private fun showInput() {
45 | setState {
46 | this.activeView = ActiveView.UserInput
47 | }
48 | }
49 |
50 | enum class ActiveView {
51 | UserInput, Result
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/web/src/main/kotlin/app/BaseComponent.kt:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import de.compeople.swn.presentation.BaseView
4 | import react.RComponent
5 | import react.RProps
6 | import react.RState
7 | import kotlin.browser.window
8 |
9 | abstract class BaseComponent(props: P) : RComponent
(props), BaseView {
10 |
11 |
12 | override fun logError(error: Throwable) {
13 | console.error(error)
14 | }
15 |
16 | override fun showError(error: Throwable) {
17 | window.alert("Error: ${error.message}")
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/web/src/main/kotlin/app/KeyValueStoreWeb.kt:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import de.compeople.swn.tarifService.KeyValueStore
4 | import kotlin.browser.localStorage
5 |
6 | class KeyValueStoreWeb : KeyValueStore {
7 |
8 | override fun setValue(key: String, value: String) {
9 | return localStorage.setItem(key, value)
10 | }
11 |
12 | override fun getValue(key: String): String? {
13 | return localStorage.getItem(key)
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/web/src/main/kotlin/app/PresenterProvider.kt:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import de.compeople.swn.presentation.displayResults.DisplayResultsPresenter
4 | import de.compeople.swn.presentation.displayResults.DisplayResultsView
5 | import de.compeople.swn.presentation.userinput.UserInputPresenter
6 | import de.compeople.swn.presentation.userinput.UserInputView
7 | import de.compeople.swn.tarifService.Rechenkern
8 | import de.compeople.swn.tarifService.TarifClient
9 | import de.compeople.swn.tarifService.TarifRepository
10 | import de.compeople.swn.tarifService.TarifService
11 | import de.compeople.swn.time.TimeService
12 |
13 | class PresenterProvider {
14 |
15 | private val timeService = TimeService()
16 | private val keyValueStore = KeyValueStoreWeb()
17 | private val tarifRepo = TarifRepository(keyValueStore)
18 | private val tarifService = TarifService(tarifRepo, TarifClient(), timeService)
19 | private val rechenkern = Rechenkern(tarifService, timeService)
20 |
21 | fun createUserInputPresenter(view: UserInputView): UserInputPresenter {
22 | return UserInputPresenter(view, rechenkern)
23 | }
24 |
25 | fun createDisplayResultsPresenter(view: DisplayResultsView): DisplayResultsPresenter {
26 | return DisplayResultsPresenter(view)
27 | }
28 | }
--------------------------------------------------------------------------------
/web/src/main/kotlin/displayresults/DisplayResultsComponent.kt:
--------------------------------------------------------------------------------
1 | package displayresults
2 |
3 | import app.BaseComponent
4 | import app.PresenterProvider
5 | import de.compeople.swn.presentation.displayResults.DisplayResultsPresenter
6 | import de.compeople.swn.presentation.displayResults.DisplayResultsView
7 | import kotlinx.html.js.onClickFunction
8 | import react.RBuilder
9 | import react.RProps
10 | import react.RState
11 | import react.dom.*
12 |
13 |
14 | typealias GoBackHandler = () -> Unit
15 |
16 | interface DisplayResultsState : RState
17 |
18 | interface DisplayResultsProps : RProps {
19 | var presenterProvider: PresenterProvider
20 | var goBackHandler: GoBackHandler
21 | var result: Int
22 | }
23 |
24 | class DisplayResultsComponent(props: DisplayResultsProps) : BaseComponent(props), DisplayResultsView {
25 |
26 | private var presenter: DisplayResultsPresenter
27 |
28 | init {
29 | presenter = props.presenterProvider.createDisplayResultsPresenter(this)
30 | }
31 |
32 | override fun RBuilder.render() {
33 | div(classes = "App-header") {
34 | h2 {
35 | +"Welcome to your insurance premium calculator"
36 | }
37 | p {
38 | +"Note: This is only a first attempt to calculate your insurance, the premium can change at every time"
39 | }
40 | }
41 | div(classes = "form-group result") {
42 | div {
43 | label {
44 | +"Result: ${props.result}"
45 | }
46 | }
47 | button(classes = "btn btn-primary") {
48 | attrs {
49 | onClickFunction = {
50 | onBack()
51 | }
52 | }
53 | +"back"
54 | }
55 | }
56 | }
57 |
58 | private fun onBack() {
59 | presenter.navigateBack()
60 | }
61 |
62 | override fun goBackToUserInputView() {
63 | props.goBackHandler()
64 | }
65 |
66 | }
67 |
68 | fun RBuilder.displayResults(presenterProvider: PresenterProvider, result: Int, goBackHandler: GoBackHandler) = child(DisplayResultsComponent::class) {
69 | attrs.presenterProvider = presenterProvider
70 | attrs.result = result
71 | attrs.goBackHandler = goBackHandler
72 | }
73 |
--------------------------------------------------------------------------------
/web/src/main/kotlin/displayresults/display-results.css:
--------------------------------------------------------------------------------
1 | .result {
2 | margin: 15px;
3 | font-size: large;
4 | }
--------------------------------------------------------------------------------
/web/src/main/kotlin/index/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/web/src/main/kotlin/index/index.kt:
--------------------------------------------------------------------------------
1 | package index
2 |
3 | import app.*
4 | import kotlinext.js.*
5 | import react.dom.*
6 | import kotlin.browser.*
7 |
8 | fun main() {
9 | requireAll(require.context("../../../src", true, js("/\\.css$/")))
10 |
11 | render(document.getElementById("root")) {
12 | app()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/web/src/main/kotlin/userinput/UserInputComponent.kt:
--------------------------------------------------------------------------------
1 | package userinput
2 |
3 | import app.BaseComponent
4 | import app.PresenterProvider
5 | import de.compeople.swn.data.Birthday
6 | import de.compeople.swn.data.Gender
7 | import de.compeople.swn.data.User
8 | import de.compeople.swn.presentation.userinput.UserInputPresenter
9 | import de.compeople.swn.presentation.userinput.UserInputValidator
10 | import de.compeople.swn.presentation.userinput.UserInputView
11 | import io.ktor.util.date.Month
12 | import kotlinx.html.InputType
13 | import kotlinx.html.id
14 | import kotlinx.html.js.onChangeFunction
15 | import kotlinx.html.js.onClickFunction
16 | import kotlinx.html.role
17 | import org.w3c.dom.HTMLInputElement
18 | import org.w3c.dom.HTMLSelectElement
19 | import org.w3c.dom.events.Event
20 | import react.*
21 | import react.dom.*
22 |
23 | typealias ShowResultHandler = (user: User, result: Int) -> Unit
24 |
25 | interface UserInputProps : RProps {
26 | var presenterProvider: PresenterProvider
27 | var user: User?
28 | var showResultHandler: ShowResultHandler
29 | }
30 |
31 | interface UserInputState : RState {
32 | var firstname: String
33 | var surname: String
34 | var birthday: Birthday
35 | var gender: Gender
36 | var loading: Boolean
37 | }
38 |
39 | fun RBuilder.userInput(presenterProvider: PresenterProvider, user: User?, showResultHandler: ShowResultHandler) = child(UserInputComponent::class) {
40 | attrs.presenterProvider = presenterProvider
41 | attrs.user = user
42 | attrs.showResultHandler = showResultHandler
43 | }
44 |
45 | object empty : ReactElement {
46 | override val props = object : RProps {}
47 | }
48 |
49 |
50 | class UserInputComponent(props: UserInputProps) : BaseComponent(props), UserInputView {
51 |
52 | private lateinit var presenter: UserInputPresenter
53 |
54 | private val user: User
55 | get() = User(firstName = state.firstname, surname = state.surname, birthday = state.birthday, gender = state.gender)
56 |
57 | override fun UserInputState.init(props: UserInputProps) {
58 | presenter = props.presenterProvider.createUserInputPresenter(this@UserInputComponent)
59 | firstname = props.user?.firstName ?: ""
60 | surname = props.user?.surname ?: ""
61 | birthday = props.user?.birthday ?: Birthday()
62 | gender = props.user?.gender ?: Gender.FEMALE
63 | loading = false
64 | }
65 |
66 | override fun RBuilder.render() {
67 | div(classes = "App-header") {
68 | h2 {
69 | +"Welcome to your insurance premium calculator"
70 | }
71 | p {
72 | +"Note: This is only a first attempt to calculate your insurance, the premium can change at every time"
73 | }
74 | }
75 | div("userInputMaske") {
76 | textInput(state.firstname, "first name")
77 | textInput(state.surname, "surname")
78 | form(classes = "form-group") {
79 | label {
80 | +"select your birthday:"
81 | }
82 | div(classes = "selection") {
83 | select(classes = "form-control custom-select") {
84 | attrs {
85 | id = "day"
86 | value = user.birthday.day.toString()
87 | onChangeFunction = ::onDayChange
88 | }
89 | for (i in 1..31) {
90 | option {
91 | +"$i"
92 | }
93 | }
94 | }
95 | select(classes = "form-control custom-select-month") {
96 | attrs {
97 | id = "month"
98 | value = Month.from(user.birthday.month - 1).toString().toLowerCase()
99 | onChangeFunction = ::onMonthChange
100 | }
101 | Month.values().map {
102 | option {
103 | +it.toString().toLowerCase()
104 | }
105 | }
106 | }
107 |
108 | select(classes = "form-control custom-select-year") {
109 | attrs {
110 | id = "year"
111 | value = user.birthday.year.toString()
112 | onChangeFunction = ::onYearChange
113 | }
114 | for (i in 1953..2000) {
115 | option {
116 | +"$i"
117 | }
118 | }
119 | }
120 | }
121 | }
122 |
123 | div(classes = "form-group") {
124 | label {
125 | +"select your gender"
126 | }
127 | div(classes = "radio") {
128 | attrs {
129 | onChangeFunction = {
130 | onGenderChecked(it)
131 | }
132 | radioOptions(Gender.FEMALE)
133 | radioOptions(Gender.MALE)
134 | radioOptions(Gender.DIVERS)
135 | }
136 | }
137 | }
138 | div(classes = "form-group") {
139 | loading(state.loading)
140 | }
141 | button(classes = "btn btn-primary") {
142 | attrs {
143 | onClickFunction = {
144 | onLoadingChange(true)
145 | calculateInsurancePremium()
146 | }
147 | }
148 | +"calculate"
149 | }
150 | }
151 | }
152 |
153 | private fun calculateInsurancePremium() {
154 | when (validateInput()) {
155 | UserInputValidator.FIRSTNAME -> showError(Throwable("Please enter your first name"))
156 | UserInputValidator.SURNAME -> showError(Throwable("Please enter your surname"))
157 | else -> {
158 | presenter.calculateInsurancePremium(user)
159 | }
160 | }
161 | }
162 |
163 | override fun showInsurancePremium(monthlyCost: Int) {
164 | state.loading = false
165 | props.showResultHandler(user, monthlyCost)
166 | }
167 |
168 | private fun validateInput(): UserInputValidator {
169 | return if (user.firstName == "" || user.firstName == "not set") {
170 | UserInputValidator.FIRSTNAME
171 | } else if (user.surname == "" || user.surname == "not set") {
172 | UserInputValidator.SURNAME
173 | } else {
174 | UserInputValidator.SUCCESS
175 | }
176 | }
177 |
178 | private fun onLoadingChange(isLoading: Boolean) {
179 | setState {
180 | loading = isLoading
181 | }
182 | }
183 |
184 |
185 | private fun onFirstnameChange(event: Event) {
186 | val target = event.target as HTMLInputElement
187 | setState {
188 | firstname = target.value
189 | }
190 | }
191 |
192 | private fun onSurnameChange(event: Event) {
193 | val target = event.target as HTMLInputElement
194 | setState {
195 | surname = target.value
196 | }
197 | }
198 |
199 | private fun onDayChange(event: Event) {
200 | val target = event.target as HTMLSelectElement
201 | setState {
202 | birthday = birthday.withDay(target.value.toInt())
203 | }
204 | }
205 |
206 | private fun onMonthChange(event: Event) {
207 | val target = event.target as HTMLSelectElement
208 | setState {
209 | birthday = birthday.withMonth(target.selectedIndex + 1)
210 | }
211 | }
212 |
213 | private fun onYearChange(event: Event) {
214 | val target = event.target as HTMLSelectElement
215 | setState {
216 | birthday = birthday.withYear(target.value.toInt())
217 | }
218 | }
219 |
220 | private fun onGenderChecked(event: Event) {
221 | val target = event.target as HTMLInputElement
222 | if (target.checked && target.id == "female") {
223 | setState {
224 | gender = Gender.FEMALE
225 | }
226 | } else if (target.checked && target.id == "male") {
227 | setState {
228 | gender = Gender.MALE
229 | }
230 | } else {
231 | setState {
232 | gender = Gender.DIVERS
233 | }
234 | }
235 | }
236 |
237 | private fun RBuilder.radioOptions(gender: Gender) {
238 | div(classes = "radio") {
239 | label {
240 | input(InputType.radio, name = "genderSelection") {
241 | attrs {
242 | id = gender.toString().toLowerCase()
243 | checked = user.gender == gender
244 | }
245 | }
246 | +gender.toString().toLowerCase()
247 | }
248 | }
249 | }
250 |
251 | private fun RBuilder.textInput(nameType: String, placeholderText: String) {
252 | div("form-group") {
253 | label(classes = "textLabel") {
254 | +placeholderText
255 | }
256 | input(InputType.text, classes = "form-control") {
257 | attrs {
258 | value = nameType
259 | onChangeFunction = {
260 | validateInputText(it, "[0-9]".toRegex(), "no numbers allowed")
261 | when (nameType) {
262 | state.firstname -> onFirstnameChange(it)
263 | else -> onSurnameChange(it)
264 | }
265 |
266 | }
267 | placeholder = placeholderText
268 | }
269 | }
270 | }
271 | }
272 |
273 | private fun RBuilder.loading(isLoading: Boolean) = if (isLoading) {
274 | spinner()
275 | } else {
276 | empty
277 | }
278 |
279 | private fun RBuilder.spinner() = div("spinner") {}
280 |
281 | private fun validateInputText(event: Event, regex: Regex, errorMessage: String) {
282 | val target = event.target as HTMLInputElement
283 | if (target.value.contains(regex)) {
284 | showError(Throwable(errorMessage))
285 | target.value = target.value.dropLast(1)
286 | }
287 | }
288 |
289 | }
290 |
291 | private fun Birthday.withDay(day: Int) = Birthday(day, this.month, this.year)
292 | private fun Birthday.withMonth(month: Int) = Birthday(this.day, month, this.year)
293 | private fun Birthday.withYear(year: Int) = Birthday(this.day, this.month, year)
294 |
--------------------------------------------------------------------------------
/web/src/main/kotlin/userinput/user-input.css:
--------------------------------------------------------------------------------
1 | .userInputMaske {
2 | margin: 15px;
3 | }
4 |
5 | .selection{
6 | display: flex;
7 | }
8 |
9 | .custom-select{
10 | width: 65px;
11 | }
12 |
13 | .custom-select-month{
14 | width: 120px;
15 | }
16 |
17 | .custom-select-year{
18 | width: 80px;
19 | }
20 |
21 | .spinner {
22 | display: inline-block;
23 | border: 0.2em solid #f3f3f3;
24 | border-top: 0.2em solid #3498db;
25 | border-radius: 50%;
26 | width: 1em;
27 | height: 1em;
28 | animation: spin 2s linear infinite;
29 | }
30 |
31 | @keyframes spin {
32 | 0% {
33 | transform: rotate(0deg);
34 | }
35 | 100% {
36 | transform: rotate(360deg);
37 | }
38 | }
--------------------------------------------------------------------------------
/web/webpack.config.d/css.js:
--------------------------------------------------------------------------------
1 | config.module.rules.push({ test: /\.css$/, use: ["style-loader", "css-loader"] });
--------------------------------------------------------------------------------
/web/webpack.config.d/minify.js:
--------------------------------------------------------------------------------
1 | if (defined.PRODUCTION) {
2 | config.plugins.push(new webpack.optimize.UglifyJsPlugin({
3 | minimize: true
4 | }));
5 | }
6 |
--------------------------------------------------------------------------------