diff --git a/build.gradle.kts b/build.gradle.kts index e3ca622..fad423f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,16 +1,16 @@ import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack -val kotlinVersion = "1.9.10" -val serializationVersion = "1.6.0" -val ktorVersion = "2.3.3" +val serializationVersion = "1.6.2" +val ktorVersion = "3.0.0-wasm2" val logbackVersion = "1.2.11" val kotlinWrappersVersion = "1.0.0-pre.621" val kmongoVersion = "4.5.0" plugins { - kotlin("multiplatform") version "1.9.10" + kotlin("multiplatform") version "1.9.21" application //to run JVM part - kotlin("plugin.serialization") version "1.9.10" + kotlin("plugin.serialization") version "1.9.21" + id("org.jetbrains.compose") version "1.6.0-alpha01" } group = "org.example" @@ -18,6 +18,8 @@ version = "1.0-SNAPSHOT" repositories { mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental") } kotlin { @@ -29,11 +31,19 @@ kotlin { binaries.executable() } } + wasmJs { + browser { + binaries.executable() + } + } sourceSets { val commonMain by getting { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion") + implementation(compose.runtime) implementation("io.ktor:ktor-client-core:$ktorVersion") + implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion") } } @@ -61,13 +71,22 @@ kotlin { val jsMain by getting { dependencies { implementation("io.ktor:ktor-client-js:$ktorVersion") - implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") implementation(project.dependencies.enforcedPlatform("org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom:$kotlinWrappersVersion")) implementation("org.jetbrains.kotlin-wrappers:kotlin-react") implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom") } } + + val wasmJsMain by getting { + dependencies { + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.material3) + @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) + implementation(compose.components.resources) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0-RC2") + } + } } } @@ -75,19 +94,25 @@ application { mainClass.set("ServerKt") } +compose.experimental { + web.application {} +} + // include JS artifacts in any JAR we generate tasks.named("jvmJar").configure { + val taskPrefix = if (project.properties.get("app.run.wasm") == "true") "wasmJs" else "js" val taskName = if (project.hasProperty("isProduction") || project.gradle.startParameter.taskNames.contains("installDist") ) { - "jsBrowserProductionWebpack" + "${taskPrefix}BrowserProductionWebpack" } else { - "jsBrowserDevelopmentWebpack" + "${taskPrefix}BrowserDevelopmentWebpack" } val webpackTask = tasks.named(taskName) dependsOn(webpackTask) - from(webpackTask.map { it.mainOutputFile.get().asFile }) // bring output file along into the JAR + from(webpackTask.map { it.outputDirectory }) // bring output file along into the JAR into("static") + duplicatesStrategy = DuplicatesStrategy.EXCLUDE } tasks { @@ -116,4 +141,4 @@ tasks.register("stage") { tasks.named("run").configure { classpath(tasks.named("jvmJar")) // so that the JS artifacts generated by `jvmJar` can be found and served -} +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index bda7801..f772412 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,7 @@ kotlin.js.compiler=ir kotlin.incremental=true kotlin.incremental.js=true kotlin.incremental.js.ir=true -kotlin.incremental.js.klib=true \ No newline at end of file +kotlin.incremental.js.klib=true +org.jetbrains.compose.experimental.wasm.enabled=true +kotlin.daemon.jvmargs=-Xmx4G +app.run.wasm=false diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 9f512ca..622d699 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -54,10 +54,10 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/trace-mapping@^0.3.17": - version "0.3.19" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811" - integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== +"@jridgewell/trace-mapping@^0.3.20": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -70,6 +70,11 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@js-joda/core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" + integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" @@ -142,9 +147,9 @@ integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== "@types/estree@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194" - integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA== + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": version "4.17.30" @@ -182,6 +187,13 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + "@types/node@*", "@types/node@>=10.0.0": version "18.6.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.6.5.tgz#06caea822caf9e59d5034b695186ee74154d2802" @@ -929,9 +941,9 @@ envinfo@^7.7.3: integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== es-module-lexer@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.0.tgz#6be9c9e0b4543a60cd166ff6f8b4e9dae0b0c16f" - integrity sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA== + version "1.4.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.4.1.tgz#41ea21b43908fe6a287ffcbe4300f790555331f5" + integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w== escalade@^3.1.1: version "3.1.1" @@ -1180,6 +1192,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -1267,6 +1284,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + he@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -1406,11 +1430,11 @@ is-binary-path@~2.1.0: binary-extensions "^2.0.0" is-core-module@^2.13.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" - integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: - has "^1.0.3" + hasown "^2.0.0" is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" @@ -1602,12 +1626,12 @@ kind-of@^6.0.2: integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== launch-editor@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.0.tgz#4c0c1a6ac126c572bd9ff9a30da1d2cae66defd7" - integrity sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ== + version "2.6.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.1.tgz#f259c9ef95cbc9425620bbbd14b468fcdb4ffe3c" + integrity sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw== dependencies: picocolors "^1.0.0" - shell-quote "^1.7.3" + shell-quote "^1.8.1" loader-runner@^4.2.0: version "4.3.0" @@ -2122,9 +2146,9 @@ resolve-from@^5.0.0: integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== resolve@^1.20.0: - version "1.22.4" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34" - integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg== + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: is-core-module "^2.13.0" path-parse "^1.0.7" @@ -2203,10 +2227,11 @@ select-hose@^2.0.0: integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== selfsigned@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.1.1.tgz#18a7613d714c0cd3385c48af0075abf3f266af61" - integrity sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ== + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== dependencies: + "@types/node-forge" "^1.3.0" node-forge "^1" send@0.18.0: @@ -2294,7 +2319,7 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -shell-quote@^1.7.3: +shell-quote@^1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== @@ -2482,20 +2507,20 @@ tapable@^2.1.1, tapable@^2.2.0: integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== terser-webpack-plugin@^5.3.7: - version "5.3.9" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" - integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== dependencies: - "@jridgewell/trace-mapping" "^0.3.17" + "@jridgewell/trace-mapping" "^0.3.20" jest-worker "^27.4.5" schema-utils "^3.1.1" serialize-javascript "^6.0.1" - terser "^5.16.8" + terser "^5.26.0" -terser@^5.16.8: - version "5.19.3" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.19.3.tgz#359baeba615aef13db4b8c4d77a2aa0d8814aa9e" - integrity sha512-pQzJ9UJzM0IgmT4FAtYI6+VqFf0lj/to58AV0Xfgg0Up37RyPG7Al+1cepC6/BVuAxR9oNb41/DL4DEoHJvTdg== +terser@^5.26.0: + version "5.26.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.26.0.tgz#ee9f05d929f4189a9c28a0feb889d96d50126fe1" + integrity sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -2801,9 +2826,9 @@ ws@8.5.0: integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== ws@^8.13.0: - version "8.13.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + version "8.16.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" + integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== ws@~8.2.3: version "8.2.3" diff --git a/settings.gradle.kts b/settings.gradle.kts index 1dbc6de..a9990cb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,9 @@ pluginManagement { repositories { + google() mavenCentral() gradlePluginPortal() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } rootProject.name = "shoppinglist" diff --git a/src/jsMain/kotlin/Api.kt b/src/commonMain/kotlin/Api.kt similarity index 96% rename from src/jsMain/kotlin/Api.kt rename to src/commonMain/kotlin/Api.kt index b8d51a6..c9f01d2 100644 --- a/src/jsMain/kotlin/Api.kt +++ b/src/commonMain/kotlin/Api.kt @@ -5,8 +5,6 @@ import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* import io.ktor.serialization.kotlinx.json.* -import kotlinx.browser.window - val jsonClient = HttpClient { install(ContentNegotiation) { json() diff --git a/src/commonMain/resources/static/index.html b/src/commonMain/resources/static/index.html index 85dca3c..ef6965c 100644 --- a/src/commonMain/resources/static/index.html +++ b/src/commonMain/resources/static/index.html @@ -6,6 +6,8 @@
- + + + \ No newline at end of file diff --git a/src/wasmJsMain/kotlin/App.kt b/src/wasmJsMain/kotlin/App.kt new file mode 100644 index 0000000..19c643b --- /dev/null +++ b/src/wasmJsMain/kotlin/App.kt @@ -0,0 +1,113 @@ +// Reworked https://github.com/russhwolf/To-Do android app + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp + +@Composable +fun ShoppingList(viewModel: ShoppingListViewModel) { + val shoppingList = viewModel.shoppingList + + LaunchedEffect(Unit) { + viewModel.fetchShoppingList() + } + + ShoppingListTheme { + Surface { + Column(Modifier.fillMaxSize()) { + Input(onCreateItem = viewModel::pushShoppingListItem) + LazyColumn { + items(shoppingList.size) { index -> + ShoppingItem( + shoppingListItem = shoppingList[index], + onDeleteClick = viewModel::removeShoppingListItem + ) + } + } + } + } + } +} + +@Composable +fun Input(onCreateItem: (ShoppingListItem) -> Unit) { + Column { + Row( + modifier = Modifier.fillMaxWidth().padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + var input by remember { mutableStateOf(TextFieldValue()) } + + fun createShoppingItem() { + val text = input.text.trim() + if (text.isBlank()) return + onCreateItem(ShoppingListItem(text.replace("!", ""), text.count { it == '!' })) + input = input.copy(text = "") + } + + OutlinedTextField( + value = input, + singleLine = true, + onValueChange = { input = it }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { createShoppingItem() }), + modifier = Modifier.weight(1f).padding(8.dp), + ) + OutlinedButton( + onClick = { createShoppingItem() }, + modifier = Modifier.padding(8.dp) + ) { + Text("Create") + } + } + Divider() + } +} + +@Composable +fun ShoppingItem(shoppingListItem: ShoppingListItem, onDeleteClick: (ShoppingListItem) -> Unit) = Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text("[${shoppingListItem.priority}] ${shoppingListItem.desc}", Modifier.weight(1f)) + IconButton( + onClick = { onDeleteClick(shoppingListItem) } + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Close", + tint = MaterialTheme.colorScheme.error + ) + } + } + Divider() +} + +@Composable +fun ShoppingListTheme(content: @Composable () -> Unit) = MaterialTheme( + colorScheme = lightColorScheme( + primary = Color(0xFF3759DF), + secondary = Color(0xFF375A86) + ), + typography = MaterialTheme.typography, + shapes = MaterialTheme.shapes, + content = content +) \ No newline at end of file diff --git a/src/wasmJsMain/kotlin/Main.kt b/src/wasmJsMain/kotlin/Main.kt new file mode 100644 index 0000000..b7fab32 --- /dev/null +++ b/src/wasmJsMain/kotlin/Main.kt @@ -0,0 +1,10 @@ +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.window.CanvasBasedWindow + +@OptIn(ExperimentalComposeUiApi::class) +fun main() { + CanvasBasedWindow("ShoppingList") { + val vm = ShoppingListViewModel() + ShoppingList(vm) + } +} \ No newline at end of file diff --git a/src/wasmJsMain/kotlin/ViewModel.kt b/src/wasmJsMain/kotlin/ViewModel.kt new file mode 100644 index 0000000..d0b9062 --- /dev/null +++ b/src/wasmJsMain/kotlin/ViewModel.kt @@ -0,0 +1,32 @@ +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.promise + +@OptIn(DelicateCoroutinesApi::class) +class ShoppingListViewModel { + var shoppingList by mutableStateOf(listOf()) + private set + + fun fetchShoppingList() { + GlobalScope.promise { + shoppingList = getShoppingList() + } + } + + fun pushShoppingListItem(shoppingListItem: ShoppingListItem) { + GlobalScope.promise { + addShoppingListItem(shoppingListItem) + shoppingList += shoppingListItem + } + } + + fun removeShoppingListItem(shoppingListItem: ShoppingListItem) { + GlobalScope.promise { + deleteShoppingListItem(shoppingListItem) + shoppingList -= shoppingListItem + } + } +} \ No newline at end of file