├── .gitignore ├── LICENSE ├── ReadMe.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── test │ │ └── de │ │ └── compeople │ │ └── swn │ │ └── placeholder.kt │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── de │ │ └── compeople │ │ └── swn │ │ ├── KeyValueStore.kt │ │ ├── PresenterProvider.kt │ │ ├── TarifApplication.kt │ │ └── view │ │ ├── BaseActivity.kt │ │ ├── DisplayResultsActivity.kt │ │ └── UserInputActivity.kt │ └── res │ ├── layout │ ├── activity_display_results.xml │ └── activity_userinput.xml │ ├── values-de-rDE │ └── strings.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── art ├── Architecture.png ├── ArchitectureBold.png ├── BlockArchitecture.png ├── androidAppGif.gif ├── iOSAppGif.gif └── webAppGifLow.gif ├── build.gradle ├── common ├── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── src │ ├── commonMain │ └── kotlin │ │ └── de.compeople.swn │ │ ├── Coroutines.kt │ │ ├── data │ │ ├── Birthday.kt │ │ ├── Gender.kt │ │ └── User.kt │ │ ├── tarifService │ │ ├── CommonTarifService.kt │ │ ├── Rechenkern.kt │ │ └── Tarif.kt │ │ └── time │ │ ├── TimeService.kt │ │ └── Timestamp.kt │ ├── commonTest │ └── kotlin │ │ └── test.de.compeople.swn │ │ ├── TarifUnitTest.kt │ │ ├── TimeServiceUnitTest.kt │ │ └── TimestampUnitTest.kt │ ├── iosMain │ └── kotlin │ │ └── de.compeople.swn │ │ ├── CoroutinesIos.kt │ │ └── time │ │ └── TimeService.kt │ ├── iosTest │ └── kotlin │ │ └── test.de.compeople.swn │ │ └── placeholder.kt │ ├── jsMain │ └── kotlin │ │ └── de │ │ └── compeople │ │ └── swn │ │ ├── coRun.kt │ │ └── time │ │ └── TimeService.kt │ ├── jsTest │ └── kotlin │ │ └── test │ │ └── de │ │ └── compeople │ │ └── swn │ │ └── placeholder.kt │ ├── jvmMain │ └── kotlin │ │ └── de │ │ └── compeople │ │ └── swn │ │ ├── coRun.kt │ │ └── time │ │ └── TimeService.kt │ └── jvmTest │ └── kotlin │ └── test │ └── de │ └── compeople │ └── swn │ └── placeholder.kt ├── commonClient ├── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── src │ ├── androidMain │ └── kotlin │ │ └── de │ │ └── compeople │ │ └── swn │ │ └── ApplicationDispatcher.kt │ ├── androidTest │ └── kotlin │ │ └── test │ │ └── de │ │ └── compeople │ │ └── swn │ │ └── placeholder.kt │ ├── commonMain │ └── kotlin │ │ └── de.compeople.swn │ │ ├── ApplicationDispatcher.kt │ │ ├── presentation │ │ ├── BaseView.kt │ │ ├── Presenter.kt │ │ ├── displayResults │ │ │ ├── DisplayresultsPresenter.kt │ │ │ └── DisplayresultsView.kt │ │ └── userinput │ │ │ ├── UserInputPresenter.kt │ │ │ └── UserInputView.kt │ │ └── tarifService │ │ ├── KeyValueStore.kt │ │ ├── TarifClient.kt │ │ ├── TarifRepository.kt │ │ └── TarifService.kt │ ├── commonTest │ ├── kotlin │ │ └── test.de.compeople.swn │ │ │ ├── displayresults │ │ │ └── DisplayResultsPresenterTest.kt │ │ │ ├── tarifService │ │ │ ├── TarifClientTest.kt │ │ │ ├── TarifRepositoryTest.kt │ │ │ └── TarifServiceTest.kt │ │ │ └── userinput │ │ │ └── UserInputPresenterTest.kt │ └── resources │ │ └── io │ │ └── mockk │ │ └── settings.properties │ ├── iosMain │ └── kotlin │ │ └── de.compeople.swn │ │ └── ApplicationDispatcher.kt │ ├── iosTest │ └── kotlin │ │ └── test.de.compeople.swn │ │ └── placeholder.kt │ ├── jsMain │ └── kotlin │ │ └── de │ │ └── compeople │ │ └── swn │ │ └── ApplicationDispatcher.kt │ └── jsTest │ └── kotlin │ └── test │ └── de │ └── compeople │ └── swn │ └── placeholder.kt ├── docs ├── favicon.ico ├── index.html ├── main.bundle.js └── manifest.json ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── native ├── iosApp │ ├── iosApp.xcodeproj │ │ └── project.pbxproj │ └── iosApp │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── DisplayResultsViewController.swift │ │ ├── Info.plist │ │ ├── KeyValueStore.swift │ │ ├── PresenterProvider.swift │ │ └── UserInputViewController.swift └── iosAppTests │ ├── Info.plist │ └── iosAppTests.swift ├── server ├── .gitignore ├── build.gradle └── src │ └── main │ ├── kotlin │ └── de │ │ └── compeople │ │ └── swn │ │ └── server │ │ ├── Application.kt │ │ └── tarif │ │ ├── TarifRouting.kt │ │ └── TarifService.kt │ └── resources │ ├── application.conf │ └── logback.xml ├── settings.gradle └── web ├── build.gradle ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src └── main │ └── kotlin │ ├── app │ ├── App.css │ ├── App.kt │ ├── BaseComponent.kt │ ├── KeyValueStoreWeb.kt │ └── PresenterProvider.kt │ ├── displayresults │ ├── DisplayResultsComponent.kt │ └── display-results.css │ ├── index │ ├── index.css │ └── index.kt │ └── userinput │ ├── UserInputComponent.kt │ └── user-input.css └── webpack.config.d ├── css.js └── minify.js /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .gradle 3 | .idea/* 4 | out 5 | *.iml 6 | local.properties 7 | app/build 8 | classes 9 | native/iosApp/iosApp.xcodeproj/project.xcworkspace 10 | native/iosApp/iosApp.xcodeproj/xcuserdata 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 compeople AG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # Kotlin multiplatform sample - swn 2 | 3 | This is an example project to test the possibilities of the Kotlin multiplatform technology. 4 | It takes a name, birthday and gender and calculate a random monthly insurance cost. 5 | It targets android, iOS and web. 6 | 7 | ##### Demo Web (written with kotlin/JS and kotlin-react) 8 | Feel free to try it yourself https://compeople.github.io/kotlin-multiplatform-sample/ \ 9 | ![webDemo](art/webAppGifLow.gif) 10 | 11 | ##### Demo android (written completely in kotlin) 12 | ![androidDemo](art/androidAppGif.gif) 13 | 14 | ##### Demo iOS (written in kotlin/native and swift) 15 | ![iOSDemo](art/iOSAppGif.gif) 16 | 17 | ## Getting started 18 | 19 | If you don't have an android sdk, you can build the application without it, using: `./gradlew build -PskipAndroid`. 20 | 21 | ### Building the Code 22 | 23 | + make sure you have an Android SDK installed 24 | + Open the project 25 | + create and add a file `local.properties` in the root directory of the project: 26 | ```sbtshell 27 | sdk.dir=yourLocalPathToAndroidSdk 28 | ``` 29 | + look under TODOs and change the url for the server call in `kotlin-multiplatform-sample/commonClient/src/commonMain/kotlin/de.compeople.swn/tarifService/TarifClient` 30 | + Run `./gradlew build` 31 | + start the server with: `./gradlew :server:run` 32 | 33 | ### Build only the server 34 | 35 | run : `:server:build -PskipAndroid` 36 | 37 | ### Running the Android app 38 | 39 | + Create a run configuration of type "Android App" 40 | + Select module "app" in the run configuration settings 41 | + run the configuration 42 | 43 | ### Running the iOS-App 44 | 45 | + start the server with: `./gradlew :server:run` 46 | + Open the XCode project under `native` 47 | + Run it as normal 48 | 49 | ### Running the website 50 | 51 | + build the website with: `./gradlew :web:build` 52 | + start with: `./gradlew :web:webpack-run` 53 | 54 | ### Running the tests 55 | First you have to adjust the url for the server call: 56 | look under TODOs and change the url for the server call in 57 | `kotlin-multiplatform-sample/commonClient/src/commonTest/kotlin/test.de.compeople.swn/tarifService/TarifClientTest` 58 | 59 | There a Tests in the common and the commonClient module. To run the Test use: 60 | + `kotlin-multiplatform-sample:common:check` 61 | + `kotlin-multiplatform-sample:commonClient:check` 62 | 63 | At the moment the tests for js are disabled, because mockk for JS is not ready yet for coroutines. 64 | 65 | ## Architecture 66 | ##### A simple block architecture for a quickly overview: 67 | ![blockarchitecture](art/BlockArchitecture.png) 68 | 69 | ##### A detail Architecture: 70 | ![Architekture](art/ArchitectureBold.png) 71 | interfaces are marked orange 72 | 73 | 74 | ## Built With 75 | + [Gradle](https://gradle.org/) - Dependency Management 76 | + [Ktor](https://ktor.io/) 77 | + [kotlin-Wrappers](https://github.com/JetBrains/kotlin-wrappers) - kotlin-react, kotlin-react-com for the web 78 | + [mockK](https://mockk.io/) - mocking library 79 | 80 | ## License 81 | This project is licensed under the MIT License 82 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | if (!hasProperty('skipAndroid')) { 3 | 4 | apply plugin: 'kotlin-android-extensions' 5 | apply plugin: 'kotlinx-serialization' 6 | apply plugin: 'com.android.application' 7 | apply plugin: 'kotlin-android' 8 | 9 | android { 10 | compileSdkVersion 28 11 | defaultConfig { 12 | applicationId "de.compeople.swn" 13 | minSdkVersion 21 14 | targetSdkVersion 28 15 | versionCode 1 16 | versionName "1.0" 17 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | packagingOptions { 26 | exclude 'META-INF/*' 27 | } 28 | } 29 | 30 | dependencies { 31 | api project(':commonClient') 32 | implementation fileTree(include: ['*.jar'], dir: 'libs') 33 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 34 | implementation "com.android.support:appcompat-v7:28.0.0" 35 | implementation "com.android.support.constraint:constraint-layout:1.1.3" 36 | implementation "io.ktor:ktor-client-android:$ktor_version" 37 | implementation "io.ktor:ktor-client-json-jvm:$ktor_version" 38 | testImplementation 'junit:junit:4.12' 39 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" 40 | testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" 41 | androidTestImplementation "com.android.support.test:runner:1.0.2" 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keepattributes *Annotation*, InnerClasses 2 | -dontnote kotlinx.serialization.SerializationKt 3 | -keep,includedescriptorclasses class de.compeople.swn.**$$serializer { *; } # <-- change package surname to your app's 4 | -keepclassmembers class de.compeople.swn.** { # <-- change package surname to your app's 5 | *** Companion; 6 | } 7 | -keepclasseswithmembers class de.compeople.swn.** { # <-- change package surname to your app's 8 | kotlinx.serialization.KSerializer serializer(...); 9 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/test/de/compeople/swn/placeholder.kt: -------------------------------------------------------------------------------- 1 | package test.de.compeople.swn 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/de/compeople/swn/KeyValueStore.kt: -------------------------------------------------------------------------------- 1 | package de.compeople.swn 2 | 3 | import android.content.SharedPreferences 4 | import de.compeople.swn.tarifService.KeyValueStore 5 | 6 | class KeyValueStoreAndroid(private val sharedPrefs: SharedPreferences) : KeyValueStore { 7 | 8 | override fun getValue(key: String): String? = 9 | sharedPrefs.getString(key, null) 10 | 11 | override fun setValue(key: String, value: String) { 12 | sharedPrefs.edit().putString(key, value).apply() 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/de/compeople/swn/PresenterProvider.kt: -------------------------------------------------------------------------------- 1 | package de.compeople.swn 2 | 3 | import android.app.Application 4 | import android.preference.PreferenceManager 5 | import de.compeople.swn.presentation.userinput.UserInputPresenter 6 | import de.compeople.swn.tarifService.* 7 | import de.compeople.swn.time.TimeService 8 | import de.compeople.swn.view.UserInputActivity 9 | 10 | class PresenterProvider(private val application: Application) { 11 | 12 | private val timeService = TimeService() 13 | private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) 14 | private val keyValueStore: KeyValueStore = KeyValueStoreAndroid(sharedPreferences) 15 | private val tarifClient = TarifClient() 16 | private val tarifRepository = TarifRepository(keyValueStore) 17 | private val tarifService = TarifService(tarifRepository, tarifClient, timeService) 18 | private val rechenkern = Rechenkern(tarifService, timeService) 19 | 20 | fun userInputPresenter(activity: UserInputActivity): UserInputPresenter = UserInputPresenter(activity, rechenkern) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/de/compeople/swn/TarifApplication.kt: -------------------------------------------------------------------------------- 1 | package de.compeople.swn 2 | 3 | import android.app.Application 4 | import de.compeople.swn.view.BaseActivity 5 | 6 | class TarifApplication : Application() { 7 | 8 | val presenterProvider by lazy { PresenterProvider(this) } 9 | } 10 | 11 | val BaseActivity.presenterProvider: PresenterProvider 12 | get() = (application as TarifApplication).presenterProvider 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/java/de/compeople/swn/view/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package de.compeople.swn.view 2 | 3 | import android.os.Bundle 4 | import android.support.v7.app.AppCompatActivity 5 | import android.util.Log 6 | import android.widget.Toast 7 | import de.compeople.swn.presentation.BaseView 8 | import de.compeople.swn.presentation.Presenter 9 | 10 | abstract class BaseActivity : AppCompatActivity(), BaseView { 11 | 12 | //protected fun presenter(init: () -> T) = init 13 | private var Presenters: List = emptyList() 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | Presenters.forEach { it.onCreate() } 18 | } 19 | 20 | override fun onDestroy() { 21 | super.onDestroy() 22 | Presenters.forEach { it.onDestroy() } 23 | } 24 | 25 | override fun logError(error: Throwable) { 26 | Log.e("LOG", error.message) 27 | } 28 | 29 | override fun showError(error: Throwable) { 30 | logError(error) 31 | Toast.makeText(applicationContext, "Error ${error.message}", Toast.LENGTH_SHORT).show() 32 | } 33 | 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/java/de/compeople/swn/view/DisplayResultsActivity.kt: -------------------------------------------------------------------------------- 1 | package de.compeople.swn.view 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.widget.Toast 6 | import de.compeople.swn.R 7 | import de.compeople.swn.presentation.displayResults.DisplayResultsView 8 | import kotlinx.android.synthetic.main.activity_display_results.* 9 | import java.util.* 10 | 11 | class DisplayResultsActivity : BaseActivity(), DisplayResultsView { 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | setContentView(R.layout.activity_display_results) 16 | val message = intent.getStringExtra("monthlyCost") 17 | createView(message) 18 | 19 | } 20 | 21 | override fun logError(error: Throwable) { 22 | Log.e("LOG", error.message) 23 | } 24 | 25 | override fun showError(error: Throwable) = 26 | Toast.makeText(applicationContext, error.message, Toast.LENGTH_SHORT).show() 27 | 28 | 29 | private fun createView(message: String) { 30 | val systemLanguage = Locale.getDefault().language 31 | if (systemLanguage == "en") { 32 | textView2.text = "$message $" 33 | } else { 34 | textView2.text = "$message €" 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/de/compeople/swn/view/UserInputActivity.kt: -------------------------------------------------------------------------------- 1 | package de.compeople.swn.view 2 | 3 | import android.app.AlertDialog 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.View 8 | import android.widget.ProgressBar 9 | import de.compeople.swn.R 10 | import de.compeople.swn.data.Birthday 11 | import de.compeople.swn.data.Gender 12 | import de.compeople.swn.data.User 13 | import de.compeople.swn.presentation.userinput.UserInputPresenter 14 | import de.compeople.swn.presentation.userinput.UserInputValidator 15 | import de.compeople.swn.presentation.userinput.UserInputView 16 | import de.compeople.swn.presenterProvider 17 | import kotlinx.android.synthetic.main.activity_userinput.* 18 | 19 | 20 | class UserInputActivity : BaseActivity(), UserInputView { 21 | 22 | lateinit var presenter: UserInputPresenter 23 | 24 | override fun onCreate(savedInstanceState: Bundle?) { 25 | super.onCreate(savedInstanceState) 26 | presenter = presenterProvider.userInputPresenter(this) 27 | 28 | setContentView(R.layout.activity_userinput) 29 | progressBar.visibility = View.GONE 30 | radioBtn_female.isChecked = true 31 | 32 | btn_continue.setOnClickListener { 33 | progressBar.visibility = View.VISIBLE 34 | sentFilledData() 35 | 36 | } 37 | } 38 | 39 | override fun onPause() { 40 | super.onPause() 41 | progressBar.visibility = View.GONE 42 | } 43 | 44 | override fun showInsurancePremium(monthlyCost: Int) { 45 | val intent = Intent(this, DisplayResultsActivity::class.java).apply { putExtra("monthlyCost", monthlyCost.toString()) } 46 | startActivity(intent) 47 | } 48 | 49 | private fun sentFilledData() { 50 | when (validateInput()) { 51 | UserInputValidator.FIRSTNAME -> showError(Throwable("Please enter your firstname")) 52 | UserInputValidator.SURNAME -> showError(Throwable("Please enter your surname")) 53 | UserInputValidator.SUCCESS -> { 54 | val user = User(editText_firstname.text.toString(), editText_surname.text.toString(), genderSelect(), 55 | Birthday(datePicker_Birthday.dayOfMonth, datePicker_Birthday.month, datePicker_Birthday.year)) 56 | presenter.calculateInsurancePremium(user) //false positive error: https://youtrack.jetbrains.com/issue/KT-26535 57 | 58 | } 59 | else -> showError(Throwable("unknown Error")) 60 | } 61 | } 62 | 63 | override fun logError(error: Throwable) { 64 | Log.e("LOG", error.message) 65 | } 66 | 67 | override fun showError(error: Throwable) { 68 | val alertDialog = AlertDialog.Builder(this@UserInputActivity).create() 69 | alertDialog.setTitle("Error") 70 | alertDialog.setMessage(error.message) 71 | alertDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "OK" 72 | ) { dialog, _ -> dialog.dismiss() } 73 | alertDialog.show() 74 | } 75 | 76 | private fun validateInput(): UserInputValidator = when { 77 | editText_firstname.text.isEmpty() -> UserInputValidator.FIRSTNAME 78 | editText_surname.text.isEmpty() -> UserInputValidator.SURNAME 79 | else -> UserInputValidator.SUCCESS 80 | } 81 | 82 | private fun genderSelect() = when { 83 | radioBtn_female.isChecked -> Gender.FEMALE 84 | radioBtn_male.isChecked -> Gender.MALE 85 | else -> Gender.DIVERS 86 | } 87 | 88 | } 89 | 90 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_display_results.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 21 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_userinput.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 17 | 33 | 48 | 56 | 57 | 68 | 73 | 78 | 83 | 84 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 136 | 145 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /native/iosApp/iosApp/DisplayResultsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayResultsController.swift 3 | // iosApp 4 | // 5 | // Created by Sabrina Wenngatz on 07.11.18. 6 | // 7 | 8 | import UIKit 9 | import commonClient 10 | 11 | class DisplayResultsViewController: UIViewController, DisplayResultsView { 12 | 13 | @IBOutlet weak var resultsLabel: UILabel! 14 | 15 | private var displayPresenter:DisplayResultsPresenter! 16 | 17 | required init?(coder aDecoder: NSCoder) { 18 | super.init(coder: aDecoder) 19 | displayPresenter = PresenterProvider.displayResultsPresenter(view: self) 20 | } 21 | 22 | 23 | func createView(message: String) { 24 | resultsLabel.text = message + "$" 25 | } 26 | 27 | //is called from displayPresenter.navigateBack() 28 | func goBackToUserInputView() { 29 | self.dismiss(animated: true, completion: nil) 30 | } 31 | 32 | @IBAction func backButton(_ sender: UIButton) { 33 | displayPresenter.navigateBack() 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /native/iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | CFBundleDevelopmentRegion 11 | $(DEVELOPMENT_LANGUAGE) 12 | CFBundleDisplayName 13 | iosApp 14 | CFBundleExecutable 15 | $(EXECUTABLE_NAME) 16 | CFBundleIdentifier 17 | $(PRODUCT_BUNDLE_IDENTIFIER) 18 | CFBundleInfoDictionaryVersion 19 | 6.0 20 | CFBundleName 21 | $(PRODUCT_NAME) 22 | CFBundlePackageType 23 | APPL 24 | CFBundleShortVersionString 25 | 1.0 26 | CFBundleVersion 27 | 1 28 | LSRequiresIPhoneOS 29 | 30 | UILaunchStoryboardName 31 | LaunchScreen 32 | UIMainStoryboardFile 33 | Main 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /native/iosApp/iosApp/KeyValueStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueStore.swift 3 | // iosApp 4 | // 5 | // Created by Sabrina Wenngatz on 20.11.18. 6 | // 7 | 8 | import Foundation 9 | import commonClient 10 | 11 | 12 | class KeyValueStoreIOS: UserDefaults, KeyValueStore { 13 | 14 | func getValue(key: String) -> String? { 15 | return UserDefaults.standard.string(forKey: key) 16 | } 17 | 18 | func setValue(key: String, value: String) { 19 | let defaults = UserDefaults.standard 20 | defaults.set(value, forKey: key) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /native/iosApp/iosApp/PresenterProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresenterProvider.swift 3 | // iosApp 4 | // 5 | 6 | import Foundation 7 | import commonClient 8 | 9 | class PresenterProvider { 10 | 11 | private let keyValueStore:KeyValueStore 12 | private let timeService:CommonTimeService 13 | private let tarifRepo:TarifRepository 14 | private let tarifService:TarifService 15 | private let rechenkern: CommonRechenkern 16 | 17 | private init() { 18 | timeService = CommonTimeService() 19 | keyValueStore = KeyValueStoreIOS() 20 | tarifRepo = TarifRepository(keyValueStore: keyValueStore) 21 | tarifService = TarifService(tarifRepository: tarifRepo, tarifClient: TarifClient(),timeService: timeService) 22 | rechenkern = CommonRechenkern(tarifService: tarifService, timeService: timeService) 23 | } 24 | 25 | private static let instance = PresenterProvider() 26 | 27 | static func userInputPresenter(view:UserInputView) -> UserInputPresenter { 28 | return UserInputPresenter(view:view, rechenkern:instance.rechenkern) 29 | } 30 | 31 | static func displayResultsPresenter(view:DisplayResultsView) -> DisplayResultsPresenter { 32 | return DisplayResultsPresenter(displayResultsView: view) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /native/iosApp/iosApp/UserInputViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserInputViewController.swift 3 | // iosApp 4 | // 5 | // Created by Sabrina Wenngatz on 07.11.18. 6 | // 7 | 8 | import UIKit 9 | import commonClient 10 | 11 | class UserInputViewController: UIViewController, UserInputView { 12 | 13 | @IBOutlet weak var userSurnameTextField: UITextField! 14 | @IBOutlet weak var userVornameTextField: UITextField! 15 | @IBOutlet weak var birthdayDatePicker: UIDatePicker! 16 | @IBOutlet weak var genderSegmentControl: UISegmentedControl! 17 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 18 | 19 | private var userPresenter:UserInputPresenter! 20 | 21 | 22 | required init?(coder aDecoder: NSCoder) { 23 | super.init(coder: aDecoder) 24 | userPresenter = PresenterProvider.userInputPresenter(view: self) 25 | 26 | } 27 | 28 | override func viewWillAppear(_ animated: Bool) { 29 | activityIndicator.hidesWhenStopped = true 30 | activityIndicator.stopAnimating() 31 | } 32 | 33 | func sentFilledData() { 34 | let userName = userSurnameTextField.text ?? "" 35 | let userFirstname = userVornameTextField.text ?? "" 36 | 37 | if(validateInput() == UserInputValidator.firstname){ 38 | showError(error: KotlinThrowable(message: "Please enter your firstname")) 39 | } 40 | if(validateInput() == UserInputValidator.surname){ 41 | showError(error: KotlinThrowable(message: "Please enter your surname")) 42 | } 43 | if(validateInput() == UserInputValidator.success){ 44 | let components = NSCalendar.current.dateComponents([.year, .month, .day], from: birthdayDatePicker.date) 45 | let birthday = CommonBirthday(day: Int32(components.day ?? 1),month: Int32(components.month ?? 1),year: Int32(components.year ?? 2000)) 46 | let user = CommonUser(firstName: userFirstname, surname: userName, gender: genderSelect(), birthday: birthday) 47 | 48 | userPresenter.calculateInsurancePremium(user: user) 49 | } 50 | else { 51 | showError(error: KotlinThrowable(message: "Unknown Error")) 52 | } 53 | } 54 | 55 | func genderSelect() -> CommonGender{ 56 | switch genderSegmentControl.selectedSegmentIndex { 57 | case 0: 58 | return .female 59 | case 1: 60 | return .male 61 | default: 62 | return .divers 63 | } 64 | } 65 | 66 | func showInsurancePremium(monthlyCost: Int32) { 67 | let resultViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ResultViewController") as! DisplayResultsViewController 68 | self.present(resultViewController, animated: true, completion: ({ 69 | resultViewController.createView(message: String(monthlyCost)) 70 | })) 71 | 72 | } 73 | 74 | func validateInput() -> UserInputValidator { 75 | if(userVornameTextField.text == ""){ 76 | return UserInputValidator.firstname 77 | } 78 | if(userSurnameTextField.text == ""){ 79 | return UserInputValidator.surname 80 | } 81 | return UserInputValidator.success 82 | } 83 | 84 | func logError(error: KotlinThrowable) { 85 | print(error.message!) 86 | } 87 | 88 | func showError(error: KotlinThrowable) { 89 | let alertController = UIAlertController(title: "Error", message: error.message ?? "Error", preferredStyle: .alert) 90 | alertController.addAction(UIAlertAction(title: "Ok", style: .destructive, handler: nil)) 91 | self.present(alertController, animated: true, completion: nil) 92 | } 93 | 94 | @IBAction func continueAction(_ sender: Any) { 95 | activityIndicator.startAnimating() 96 | sentFilledData() 97 | } 98 | 99 | } 100 | 101 | -------------------------------------------------------------------------------- /native/iosAppTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | -------------------------------------------------------------------------------- /native/iosAppTests/iosAppTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import app 3 | 4 | class iosAppTests: XCTestCase { 5 | func testExample() { 6 | assert(Sample().checkMe() == 7) 7 | } 8 | } -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle 2 | /.idea 3 | /out 4 | /build 5 | *.iml 6 | *.ipr 7 | *.iws 8 | -------------------------------------------------------------------------------- /server/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlinx-serialization' 2 | //apply plugin: 'java' 3 | apply plugin: 'kotlin' 4 | apply plugin: 'application' 5 | apply plugin: 'com.github.johnrengelman.shadow' 6 | 7 | mainClassName = "io.ktor.server.netty.EngineMain" 8 | 9 | repositories { 10 | mavenLocal() 11 | jcenter() 12 | } 13 | 14 | dependencies { 15 | 16 | compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 17 | compile "io.ktor:ktor-server-netty:$ktor_version" 18 | compile "io.ktor:ktor-client-json-jvm:$ktor_version" 19 | 20 | compile "ch.qos.logback:logback-classic:1.2.1" 21 | compile project(':common') 22 | 23 | testCompile "io.ktor:ktor-server-tests:$ktor_version" 24 | } 25 | 26 | shadowJar { 27 | manifest { 28 | attributes 'Main-Class': mainClassName 29 | } 30 | } -------------------------------------------------------------------------------- /server/src/main/kotlin/de/compeople/swn/server/Application.kt: -------------------------------------------------------------------------------- 1 | package de.compeople.swn.server 2 | 3 | import de.compeople.swn.server.tarif.tarif 4 | import io.ktor.application.Application 5 | import io.ktor.application.install 6 | import io.ktor.features.CORS 7 | import io.ktor.routing.routing 8 | import java.time.Duration 9 | 10 | fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) 11 | 12 | @Suppress("unused") // Referenced in application.conf 13 | @kotlin.jvm.JvmOverloads 14 | fun Application.module(testing: Boolean = false) { 15 | 16 | install(CORS) { 17 | anyHost() 18 | maxAge = Duration.ofDays(1) 19 | } 20 | 21 | routing { 22 | tarif() 23 | } 24 | 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /server/src/main/kotlin/de/compeople/swn/server/tarif/TarifRouting.kt: -------------------------------------------------------------------------------- 1 | package de.compeople.swn.server.tarif 2 | 3 | import de.compeople.swn.tarifService.Tarif 4 | import io.ktor.application.call 5 | import io.ktor.http.ContentType 6 | import io.ktor.response.respondText 7 | import io.ktor.routing.Routing 8 | import io.ktor.routing.get 9 | import io.ktor.routing.route 10 | import kotlinx.serialization.json.Json 11 | 12 | fun Routing.tarif() { 13 | 14 | route("tarif") { 15 | 16 | get { 17 | val tarif = TarifService.getTarif() 18 | call.respondText(Json.stringify(Tarif.serializer(), tarif), ContentType.Application.Json) 19 | } 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/src/main/kotlin/de/compeople/swn/server/tarif/TarifService.kt: -------------------------------------------------------------------------------- 1 | package de.compeople.swn.server.tarif 2 | 3 | import de.compeople.swn.tarifService.CommonTarifService 4 | import de.compeople.swn.tarifService.Tarif 5 | import de.compeople.swn.tarifService.TarifEntry 6 | import de.compeople.swn.time.Timestamp 7 | 8 | object TarifService : CommonTarifService { 9 | 10 | override suspend fun getTarif(): Tarif { 11 | val randomTarifEntries = listOf( 12 | TarifEntry(18, getRandomNumber(), getRandomNumber(), getRandomNumber()), 13 | TarifEntry(25, getRandomNumber(), getRandomNumber(), getRandomNumber()), 14 | TarifEntry(55, getRandomNumber(), getRandomNumber(), getRandomNumber())) 15 | return Tarif(randomTarifEntries, Timestamp(System.currentTimeMillis())) 16 | } 17 | 18 | private fun getRandomNumber(): Int { 19 | return (100..500).shuffled().first() 20 | } 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /server/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | ktor { 2 | deployment { 3 | port = 8080 4 | } 5 | application { 6 | modules = [ de.compeople.swn.server.ApplicationKt.module ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /server/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | resolutionStrategy { 3 | eachPlugin { 4 | if (requested.id.id == "kotlin-multiplatform") { 5 | useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}") 6 | } else if (requested.id.id == "org.jetbrains.kotlin.frontend") { 7 | useModule("org.jetbrains.kotlin:kotlin-frontend-plugin:${requested.version}") 8 | } else if (requested.id.id == "com.android.application") { 9 | useModule("com.android.tools.build:gradle:${requested.version}") 10 | } 11 | } 12 | } 13 | 14 | repositories { 15 | mavenCentral() 16 | maven { url 'https://plugins.gradle.org/m2/' } 17 | maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } 18 | } 19 | } 20 | 21 | 22 | enableFeaturePreview('GRADLE_METADATA') 23 | 24 | include ':app' 25 | include ':common' 26 | include ':commonClient' 27 | include ':web' 28 | include ':server' 29 | -------------------------------------------------------------------------------- /web/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin2js' 2 | apply plugin: 'kotlin-dce-js' 3 | apply plugin: 'kotlinx-serialization' 4 | apply plugin: 'org.jetbrains.kotlin.frontend' 5 | 6 | repositories { 7 | google() 8 | mavenCentral() 9 | jcenter() 10 | maven { url 'https://kotlin.bintray.com/kotlin-js-wrappers' } 11 | maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } 12 | maven { url "https://kotlin.bintray.com/kotlinx" } 13 | maven { url "http://dl.bintray.com/kotlin/kotlinx.html" } 14 | } 15 | 16 | 17 | dependencies { 18 | compile project(':common') 19 | compile project(':commonClient') 20 | compile "org.jetbrains.kotlin:kotlin-stdlib-js:$kotlin_version" 21 | compile "io.ktor:ktor-client-js:$ktor_version" 22 | compile "io.ktor:ktor-client-json-js:$ktor_version" 23 | 24 | compile "org.jetbrains:kotlin-react:$kotlin_react" 25 | compile "org.jetbrains:kotlin-react-dom:$kotlin_react" 26 | 27 | testCompile "org.jetbrains.kotlin:kotlin-test-js:$kotlin_version" 28 | } 29 | 30 | kotlinFrontend { 31 | downloadNodeJsVersion = "latest" 32 | define "PRODUCTION", false 33 | 34 | webpackBundle { 35 | bundleName = "main" 36 | sourceMapEnabled = true // enable/disable source maps 37 | contentPath = file('public') 38 | publicPath = "/" // web prefix 39 | host = "localhost" // dev server host 40 | port = 8088 // dev server port 41 | // proxyUrl = "" | "http://...." // URL to be proxied, useful to proxy backend webserver 42 | // stats = "errors-only" // log level 43 | } 44 | 45 | npm { 46 | dependency("react", "^16.6.3") 47 | dependency("react-dom", "^16.6.3") 48 | dependency("core-js", "2.6.4") 49 | dependency("text-encoding", "^0.7.0") 50 | devDependency("style-loader", "0.23.1") 51 | devDependency("css-loader", "2.1.0") 52 | devDependency("file-loader", "3.0.1") 53 | devDependency("webpack-cli", "3.1.2") 54 | } 55 | } 56 | 57 | // kotlin2js configuration 58 | compileKotlin2Js { 59 | kotlinOptions { 60 | metaInfo = true 61 | outputFile = "$project.buildDir.path/js/${project.name}.js" 62 | sourceMap = true 63 | moduleKind = 'commonjs' 64 | main = "call" 65 | } 66 | } 67 | 68 | compileTestKotlin2Js { 69 | kotlinOptions.metaInfo = true 70 | kotlinOptions.outputFile = "$project.buildDir.path/js-tests/${project.name}-tests.js" 71 | kotlinOptions.sourceMap = true 72 | kotlinOptions.moduleKind = 'commonjs' 73 | kotlinOptions.main = "call" 74 | } 75 | 76 | 77 | task wrapper(type: Wrapper) { 78 | gradleVersion = '4.10.1' 79 | } 80 | -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compeople/kotlin-multiplatform-sample/e2d742c6430703e9a385ac3cea6648812a699652/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 13 | 14 | 15 | 24 | Mpp Insurance Calc 25 | 26 | 27 | 30 |
31 | 32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Kotlin App", 3 | "name": "Create React Kotlin App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /web/src/main/kotlin/app/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #9d9da1; 3 | } 4 | 5 | .App-header { 6 | background-color: #006e96; 7 | text-align: center; 8 | height: 160px; 9 | padding: 20px; 10 | color: white; 11 | } 12 | 13 | .App-intro { 14 | font-size: large; 15 | } 16 | 17 | .App-ticker { 18 | font-size: medium; 19 | } 20 | -------------------------------------------------------------------------------- /web/src/main/kotlin/app/App.kt: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import de.compeople.swn.data.User 4 | import displayresults.displayResults 5 | import react.* 6 | import userinput.userInput 7 | 8 | 9 | fun RBuilder.app() = child(App::class) {} 10 | 11 | interface AppProps : RProps 12 | 13 | interface AppState : RState { 14 | var activeView: App.ActiveView 15 | var user: User 16 | var premium: Int 17 | } 18 | 19 | 20 | class App : RComponent() { 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 | --------------------------------------------------------------------------------