From 35cbb6aebd61ab09b628b907076f52d8c3f69d8d Mon Sep 17 00:00:00 2001 From: feragusper Date: Wed, 14 Jan 2026 11:41:41 +0100 Subject: [PATCH 1/3] feat(dependencies): reorganize and update project dependencies for improved structure and functionality --- apps/mobile/build.gradle.kts | 35 +++++++------------ apps/wear/build.gradle.kts | 9 ++--- apps/web/build.gradle.kts | 23 ++++++------ build.gradle.kts | 2 +- .../presentation/web/build.gradle.kts | 7 ++-- features/chatbot/domain/build.gradle.kts | 17 --------- .../history/presentation/web/build.gradle.kts | 4 +-- features/home/domain/build.gradle.kts | 17 --------- .../home/presentation/web/build.gradle.kts | 5 ++- .../presentation/web/build.gradle.kts | 6 ++-- .../stats/presentation/web/build.gradle.kts | 8 +---- gradle/libs.versions.toml | 2 ++ .../architecture/domain/build.gradle.kts | 23 ------------ .../authentication/data/web/build.gradle.kts | 2 +- .../authentication/domain/build.gradle.kts | 13 ------- .../presentation/web/build.gradle.kts | 13 ++----- libraries/design/common/build.gradle.kts | 6 ---- libraries/logging/build.gradle.kts | 8 ----- libraries/smokes/data/web/build.gradle.kts | 4 +-- libraries/smokes/domain/build.gradle.kts | 14 +++----- libraries/wear/data/build.gradle.kts | 3 -- 21 files changed, 53 insertions(+), 168 deletions(-) diff --git a/apps/mobile/build.gradle.kts b/apps/mobile/build.gradle.kts index ee4b3e2..83772c2 100644 --- a/apps/mobile/build.gradle.kts +++ b/apps/mobile/build.gradle.kts @@ -161,39 +161,30 @@ fun properties(propertiesFileName: String): Properties { } dependencies { - // Base AndroidX libraries bundle. - implementation(libs.bundles.androidx.base) - // Use Compose BOM for consistent Compose library versions. - implementation(platform(libs.androidx.compose.bom)) - // Jetpack Compose libraries bundle. - implementation(libs.bundles.compose) - // Material 3 design components. - implementation(libs.material3) - // AndroidX Navigation libraries bundle. - implementation(libs.bundles.androidx.navigation) - // Dagger Hilt for dependency injection. - implementation(libs.hilt) - // Timber for logging. - implementation(libs.timber) - // Project modules. - implementation(project(":libraries:design:mobile")) implementation(project(":libraries:architecture:presentation:mobile")) implementation(project(":libraries:authentication:domain")) + implementation(project(":libraries:design:mobile")) implementation(project(":libraries:smokes:domain")) implementation(project(":features:authentication:presentation:mobile")) + implementation(project(":features:chatbot:presentation")) + implementation(project(":features:chatbot:domain")) implementation(project(":features:history:presentation:mobile")) implementation(project(":features:home:presentation:mobile")) implementation(project(":features:home:domain")) implementation(project(":features:settings:presentation:mobile")) implementation(project(":features:stats:presentation:mobile")) - implementation(project(":features:chatbot:presentation")) - implementation(project(":features:chatbot:domain")) - // Hilt annotation processor. - kapt(libs.hilt.compiler) - // Include devtools module only in debug builds. + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.androidx.base) + implementation(libs.bundles.compose) + implementation(libs.material3) + implementation(libs.bundles.androidx.navigation) + implementation(libs.hilt) + implementation(libs.timber) + implementation(libs.animated.navigation.bar) + debugImplementation(project(":features:devtools:presentation")) - implementation(libs.animated.navigation.bar) + kapt(libs.hilt.compiler) } // Task to print the current version name to the console. diff --git a/apps/wear/build.gradle.kts b/apps/wear/build.gradle.kts index 56dc532..31135b2 100644 --- a/apps/wear/build.gradle.kts +++ b/apps/wear/build.gradle.kts @@ -136,22 +136,23 @@ fun properties(propertiesFileName: String): Properties { dependencies { implementation(project(":libraries:architecture:presentation:mobile")) implementation(project(":libraries:architecture:common")) + implementation(project(":libraries:design:mobile")) + implementation(project(":libraries:smokes:data:mobile")) implementation(project(":libraries:wear:domain")) implementation(project(":libraries:wear:data")) - implementation(project(":libraries:smokes:data:mobile")) - implementation(libs.bundles.androidx.base) implementation(platform(libs.androidx.compose.bom)) + implementation(libs.bundles.androidx.base) implementation(libs.bundles.compose) implementation(libs.hilt) implementation(libs.timber) - implementation(project(":libraries:design:mobile")) implementation(libs.androidx.tiles) implementation(libs.horologist.composables) implementation(libs.horologist.tiles) implementation(libs.horologist.compose.tools) implementation(libs.androidx.protolayout.material) implementation(libs.androidx.protolayout.core) - kapt(libs.hilt.compiler) implementation(libs.play.services.wearable) implementation(libs.androidx.tiles.material) + + kapt(libs.hilt.compiler) } \ No newline at end of file diff --git a/apps/web/build.gradle.kts b/apps/web/build.gradle.kts index 4c7509e..1aaaddc 100644 --- a/apps/web/build.gradle.kts +++ b/apps/web/build.gradle.kts @@ -61,23 +61,22 @@ kotlin { sourceSets { val jsMain by getting { dependencies { - implementation(compose.runtime) - implementation(compose.html.core) - implementation(libs.kotlinx.coroutines.core) - implementation(project(":libraries:architecture:domain")) - implementation(project(":features:home:domain")) - implementation(project(":features:home:presentation:web")) - implementation(project(":features:history:presentation:web")) - implementation(project(":features:authentication:presentation:web")) - implementation(project(":features:stats:presentation:web")) - implementation(project(":features:settings:presentation:web")) - implementation(project(":libraries:smokes:domain")) - implementation(project(":libraries:smokes:data:web")) implementation(project(":libraries:authentication:domain")) implementation(project(":libraries:authentication:data:web")) implementation(project(":libraries:design:web")) + implementation(project(":libraries:smokes:domain")) + implementation(project(":libraries:smokes:data:web")) + implementation(project(":features:authentication:presentation:web")) + implementation(project(":features:history:presentation:web")) + implementation(project(":features:home:domain")) + implementation(project(":features:home:presentation:web")) + implementation(project(":features:settings:presentation:web")) + implementation(project(":features:stats:presentation:web")) + implementation(compose.runtime) + implementation(compose.html.core) + implementation(libs.kotlinx.coroutines.core) implementation(libs.gitlive.firebase.auth) implementation(libs.firebase.app) } diff --git a/build.gradle.kts b/build.gradle.kts index cc2f6b6..61377b4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,7 +26,7 @@ buildscript { plugins { // Google Services plugin is declared here but not applied by default. // Apply it in the respective modules as needed. - id("com.google.gms.google-services") version "4.4.3" apply false + id("com.google.gms.google-services") version "4.4.4" apply false // Apply the SonarQube plugin globally for static code analysis. sonarqube diff --git a/features/authentication/presentation/web/build.gradle.kts b/features/authentication/presentation/web/build.gradle.kts index 0d1fa6f..587311b 100644 --- a/features/authentication/presentation/web/build.gradle.kts +++ b/features/authentication/presentation/web/build.gradle.kts @@ -10,14 +10,13 @@ kotlin { sourceSets { val jsMain by getting { dependencies { - implementation(libs.kotlinx.coroutines.core) - implementation(compose.runtime) - implementation(compose.html.core) - implementation(project(":libraries:authentication:domain")) implementation(project(":libraries:authentication:data:web")) implementation(project(":libraries:authentication:presentation:web")) + implementation(compose.runtime) + implementation(compose.html.core) + implementation(libs.kotlinx.coroutines.core) implementation(libs.gitlive.firebase.auth) implementation(libs.firebase.app) } diff --git a/features/chatbot/domain/build.gradle.kts b/features/chatbot/domain/build.gradle.kts index 4802707..472eb4a 100644 --- a/features/chatbot/domain/build.gradle.kts +++ b/features/chatbot/domain/build.gradle.kts @@ -22,23 +22,6 @@ kotlin { } } - val jvmMain by getting { - dependencies { - implementation(libs.javax.inject) - implementation(project(":libraries:smokes:domain")) - } - } - - val jvmTest by getting { - dependencies { - implementation(libs.junit.jupiter.api) - implementation(libs.junit.jupiter.params) - runtimeOnly(libs.junit.jupiter.engine) - implementation(libs.kluent) - implementation(libs.coroutines.test) - implementation(libs.app.cash.turbine) - } - } } } diff --git a/features/history/presentation/web/build.gradle.kts b/features/history/presentation/web/build.gradle.kts index ae14053..d55831b 100644 --- a/features/history/presentation/web/build.gradle.kts +++ b/features/history/presentation/web/build.gradle.kts @@ -15,10 +15,10 @@ kotlin { implementation(compose.runtime) implementation(compose.html.core) - implementation(project(":libraries:design:web")) implementation(project(":libraries:architecture:domain")) - implementation(project(":libraries:smokes:domain")) implementation(project(":libraries:authentication:domain")) + implementation(project(":libraries:design:web")) + implementation(project(":libraries:smokes:domain")) } } } diff --git a/features/home/domain/build.gradle.kts b/features/home/domain/build.gradle.kts index 70f5cd0..1442534 100644 --- a/features/home/domain/build.gradle.kts +++ b/features/home/domain/build.gradle.kts @@ -21,23 +21,6 @@ kotlin { } } - val jvmMain by getting { - dependencies { - implementation(libs.javax.inject) - } - } - - val jvmTest by getting { - dependencies { - implementation(libs.junit.jupiter.api) - implementation(libs.junit.jupiter.params) - runtimeOnly(libs.junit.jupiter.engine) - - implementation(libs.kluent) - implementation(libs.coroutines.test) - implementation(libs.app.cash.turbine) - } - } } } diff --git a/features/home/presentation/web/build.gradle.kts b/features/home/presentation/web/build.gradle.kts index c1902d9..ac6dc8f 100644 --- a/features/home/presentation/web/build.gradle.kts +++ b/features/home/presentation/web/build.gradle.kts @@ -11,16 +11,15 @@ kotlin { val jsMain by getting { dependencies { implementation(project(":libraries:architecture:domain")) - implementation(project(":features:home:domain")) implementation(project(":libraries:smokes:domain")) implementation(project(":libraries:authentication:domain")) implementation(project(":libraries:logging")) implementation(project(":libraries:design:web")) - - implementation(libs.kotlinx.coroutines.core) + implementation(project(":features:home:domain")) implementation(compose.runtime) implementation(compose.html.core) + implementation(libs.kotlinx.coroutines.core) } } } diff --git a/features/settings/presentation/web/build.gradle.kts b/features/settings/presentation/web/build.gradle.kts index 9813df9..0b395bc 100644 --- a/features/settings/presentation/web/build.gradle.kts +++ b/features/settings/presentation/web/build.gradle.kts @@ -13,12 +13,12 @@ kotlin { sourceSets { val jsMain by getting { dependencies { - implementation(compose.runtime) - implementation(compose.html.core) - implementation(project(":libraries:design:web")) implementation(project(":libraries:authentication:domain")) implementation(project(":libraries:authentication:presentation:web")) + + implementation(compose.runtime) + implementation(compose.html.core) } } } diff --git a/features/stats/presentation/web/build.gradle.kts b/features/stats/presentation/web/build.gradle.kts index 6ffc461..f337306 100644 --- a/features/stats/presentation/web/build.gradle.kts +++ b/features/stats/presentation/web/build.gradle.kts @@ -12,19 +12,13 @@ kotlin { sourceSets { val jsMain by getting { dependencies { - // --- Architecture / state handling --- implementation(project(":libraries:architecture:domain")) implementation(project(":libraries:design:web")) - - // --- Domain --- implementation(project(":libraries:smokes:domain")) - // --- Coroutines --- - implementation(libs.kotlinx.coroutines.core) - - // --- Compose Web --- implementation(compose.runtime) implementation(compose.html.core) + implementation(libs.kotlinx.coroutines.core) implementation(npm("chart.js", "4.4.1")) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6231672..1b429d2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ espressoCore = "3.7.0" firebaseApp = "2.4.0" firebaseAuth = "2.4.0" firebaseBOM = "34.7.0" +firebaseFirestore = "2.1.0" generativeai = "0.9.0" googleid = "1.1.1" gradle = "8.13.2" @@ -86,6 +87,7 @@ firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "fir firebase-firestore = { module = "com.google.firebase:firebase-firestore" } generativeai = { module = "com.google.ai.client.generativeai:generativeai", version.ref = "generativeai" } gitlive-firebase-auth = { module = "dev.gitlive:firebase-auth", version.ref = "firebaseAuth" } +gitlive-firebase-firestore = { module = "dev.gitlive:firebase-firestore", version.ref = "firebaseFirestore" } hilt = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" } hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } diff --git a/libraries/architecture/domain/build.gradle.kts b/libraries/architecture/domain/build.gradle.kts index ab576fc..d06883c 100644 --- a/libraries/architecture/domain/build.gradle.kts +++ b/libraries/architecture/domain/build.gradle.kts @@ -22,30 +22,7 @@ kotlin { api(libs.kotlinx.datetime) } } - val commonTest by getting { - dependencies { - implementation(kotlin("test")) - } - } - - val jvmMain by getting - val jvmTest by getting { - dependencies { - implementation(libs.junit.jupiter.api) - implementation(libs.junit.jupiter.params) - runtimeOnly(libs.junit.jupiter.engine) - - implementation(libs.kluent) - implementation(libs.coroutines.test) - implementation(libs.app.cash.turbine) - } - } - val jsMain by getting - val jsTest by getting - - val wasmJsMain by getting - val wasmJsTest by getting } } diff --git a/libraries/authentication/data/web/build.gradle.kts b/libraries/authentication/data/web/build.gradle.kts index 44575f6..f797c41 100644 --- a/libraries/authentication/data/web/build.gradle.kts +++ b/libraries/authentication/data/web/build.gradle.kts @@ -13,7 +13,7 @@ kotlin { val commonMain by getting { dependencies { implementation(project(":libraries:authentication:domain")) - implementation("dev.gitlive:firebase-auth:1.13.0") // o la versión que uses en el resto + implementation(libs.gitlive.firebase.auth) // o la versión que uses en el resto } } } diff --git a/libraries/authentication/domain/build.gradle.kts b/libraries/authentication/domain/build.gradle.kts index d56e469..96da031 100644 --- a/libraries/authentication/domain/build.gradle.kts +++ b/libraries/authentication/domain/build.gradle.kts @@ -26,19 +26,6 @@ kotlin { } } - val jvmMain by getting - - val jvmTest by getting { - dependencies { - implementation(libs.bundles.test) - } - } - - val jsMain by getting - val jsTest by getting - - val wasmJsMain by getting - val wasmJsTest by getting } } diff --git a/libraries/authentication/presentation/web/build.gradle.kts b/libraries/authentication/presentation/web/build.gradle.kts index db79615..ccfbd47 100644 --- a/libraries/authentication/presentation/web/build.gradle.kts +++ b/libraries/authentication/presentation/web/build.gradle.kts @@ -12,22 +12,15 @@ kotlin { sourceSets { val jsMain by getting { dependencies { - // ─────────── Domain ─────────── implementation(project(":libraries:authentication:domain")) - // ─────────── Coroutines ─────────── - implementation(libs.kotlinx.coroutines.core) - - // ─────────── Compose Web (DOM) ─────────── implementation(compose.runtime) implementation(compose.html.core) - - // ─────────── Firebase (GitLive, JS) ─────────── - implementation("dev.gitlive:firebase-auth:1.13.0") - implementation("dev.gitlive:firebase-app:1.13.0") + implementation(libs.kotlinx.coroutines.core) + implementation(libs.gitlive.firebase.auth) + implementation(libs.firebase.app) } } - val jsTest by getting } } \ No newline at end of file diff --git a/libraries/design/common/build.gradle.kts b/libraries/design/common/build.gradle.kts index bb230e3..05d64fb 100644 --- a/libraries/design/common/build.gradle.kts +++ b/libraries/design/common/build.gradle.kts @@ -8,12 +8,6 @@ kotlin { js(IR) { browser() } - - sourceSets { - commonMain.dependencies { - // nada android/web acá; solo kotlin común - } - } } android { diff --git a/libraries/logging/build.gradle.kts b/libraries/logging/build.gradle.kts index 8ad3544..418b2b3 100644 --- a/libraries/logging/build.gradle.kts +++ b/libraries/logging/build.gradle.kts @@ -3,10 +3,8 @@ plugins { } kotlin { - // Android / JVM jvm() - // Web (Compose Web / JS IR) js(IR) { browser() } @@ -17,11 +15,5 @@ kotlin { implementation(libs.kermit) } } - - val commonTest by getting { - dependencies { - implementation(kotlin("test")) - } - } } } \ No newline at end of file diff --git a/libraries/smokes/data/web/build.gradle.kts b/libraries/smokes/data/web/build.gradle.kts index e539b3b..11bfb18 100644 --- a/libraries/smokes/data/web/build.gradle.kts +++ b/libraries/smokes/data/web/build.gradle.kts @@ -14,8 +14,8 @@ kotlin { dependencies { implementation(project(":libraries:smokes:domain")) implementation(project(":libraries:architecture:domain")) - implementation("dev.gitlive:firebase-auth:2.1.0") - implementation("dev.gitlive:firebase-firestore:2.1.0") + implementation(libs.gitlive.firebase.auth) + implementation(libs.gitlive.firebase.firestore) implementation(libs.kotlinx.serialization.json) } } diff --git a/libraries/smokes/domain/build.gradle.kts b/libraries/smokes/domain/build.gradle.kts index f901058..a23ff80 100644 --- a/libraries/smokes/domain/build.gradle.kts +++ b/libraries/smokes/domain/build.gradle.kts @@ -1,3 +1,7 @@ +@file:OptIn(ExperimentalWasmDsl::class) + +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + plugins { id("kmp-lib") } @@ -29,19 +33,9 @@ kotlin { implementation(libs.javax.inject) // solo si todavía usás @Inject en código jvm específico } } - - val jvmTest by getting { - dependencies { - implementation(libs.bundles.test) - } - } } } -dependencies { - add("jvmTestImplementation", platform(libs.junit.bom)) -} - tasks.withType().configureEach { useJUnitPlatform() } \ No newline at end of file diff --git a/libraries/wear/data/build.gradle.kts b/libraries/wear/data/build.gradle.kts index 4295488..fae1ed9 100644 --- a/libraries/wear/data/build.gradle.kts +++ b/libraries/wear/data/build.gradle.kts @@ -21,9 +21,6 @@ dependencies { implementation(libs.hilt) kapt(libs.hilt.compiler) - // Only if used - // implementation(libs.androidx.compose.runtime) - testImplementation(platform(libs.junit.bom)) testImplementation(libs.bundles.test) } \ No newline at end of file From 13966f96c2cd084f7a8acfd54b117831c7b6b699 Mon Sep 17 00:00:00 2001 From: feragusper Date: Wed, 14 Jan 2026 12:17:17 +0100 Subject: [PATCH 2/3] feat(home): implement smoke time editing functionality and cumulative hourly stats --- .../home/presentation/web/HomeWebScreen.kt | 80 +++++++++++++++++-- .../stats/presentation/web/StatsWebScreen.kt | 20 ++++- 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/features/home/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/home/presentation/web/HomeWebScreen.kt b/features/home/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/home/presentation/web/HomeWebScreen.kt index e035c32..e71bfbc 100644 --- a/features/home/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/home/presentation/web/HomeWebScreen.kt +++ b/features/home/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/home/presentation/web/HomeWebScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.remember import com.feragusper.smokeanalytics.features.home.presentation.web.mvi.HomeIntent import com.feragusper.smokeanalytics.features.home.presentation.web.mvi.HomeResult @@ -21,7 +22,9 @@ import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime +import org.jetbrains.compose.web.attributes.disabled import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.dom.Input import org.jetbrains.compose.web.dom.P import org.jetbrains.compose.web.dom.Span import org.jetbrains.compose.web.dom.Text @@ -62,6 +65,11 @@ fun HomeWebScreen( fun HomeViewState.Render( onIntent: (HomeIntent) -> Unit, ) { + val tz = remember { TimeZone.currentSystemDefault() } + + val editing = remember { mutableStateMapOf() } + val draftTime = remember { mutableStateMapOf() } + Div { Div(attrs = { classes(SmokeWebStyles.statsRow) }) { StatCard( @@ -103,19 +111,77 @@ fun HomeViewState.Render( } else { Div(attrs = { classes(SmokeWebStyles.list) }) { latestSmokes.forEach { smoke -> - val local = smoke.date.toLocalDateTime(TimeZone.currentSystemDefault()) + val id = smoke.id + val isEditing = editing[id] == true + + val local = smoke.date.toLocalDateTime(tz) val hh = local.hour.toString().padStart(2, '0') val mm = local.minute.toString().padStart(2, '0') + val timeLabel = "$hh:$mm" + val subtitle = smoke.timeElapsedSincePreviousSmoke.let { (h, m) -> if (h > 0) "After $h hours and $m minutes" else "After $m minutes" } - SmokeRow( - time = "$hh:$mm", - subtitle = subtitle, - onEdit = { onIntent(HomeIntent.EditSmoke(smoke.id, smoke.date)) }, - onDelete = { onIntent(HomeIntent.DeleteSmoke(smoke.id)) } - ) + if (!isEditing) { + SmokeRow( + time = timeLabel, + subtitle = subtitle, + onEdit = { + editing[id] = true + draftTime[id] = smoke.date.toTimeInputValue(tz) + }, + onDelete = { onIntent(HomeIntent.DeleteSmoke(id)) } + ) + } else { + val draft = draftTime[id] ?: smoke.date.toTimeInputValue(tz) + + SurfaceCard { + Div(attrs = { classes(SmokeWebStyles.sectionTitle) }) { + Text("Edit smoke time") + } + + Input( + type = org.jetbrains.compose.web.attributes.InputType.Time, + attrs = { + classes(SmokeWebStyles.dateInput) + value(draft) + if (displayLoading) disabled() + onInput { ev -> draftTime[id] = ev.value } + } + ) + + Div { + PrimaryButton( + text = "Apply", + enabled = !displayLoading, + onClick = { + val newTime = draftTime[id] ?: return@PrimaryButton + val dateValue = smoke.date.toDateInputValue(tz) + + val newInstant = dateTimeInputsToInstant( + dateValue = dateValue, + timeValue = newTime, + timeZone = tz, + ) + + onIntent(HomeIntent.EditSmoke(id, newInstant)) + editing[id] = false + draftTime.remove(id) + } + ) + Span { Text(" ") } + GhostButton( + text = "Cancel", + enabled = !displayLoading, + onClick = { + editing[id] = false + draftTime.remove(id) + } + ) + } + } + } } } } diff --git a/features/stats/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/stats/presentation/web/StatsWebScreen.kt b/features/stats/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/stats/presentation/web/StatsWebScreen.kt index 663837d..4e11fb4 100644 --- a/features/stats/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/stats/presentation/web/StatsWebScreen.kt +++ b/features/stats/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/stats/presentation/web/StatsWebScreen.kt @@ -190,7 +190,7 @@ private fun StatsWebContent( StatsPeriod.DAY -> LineChartJs( canvasId = chartId, title = "Today", - data = stats.hourly + data = stats.hourly.toCumulativeHourly() ) StatsPeriod.WEEK -> BarChartJs( @@ -368,4 +368,22 @@ private fun barDataset( ds["borderWidth"] = 1 ds["fill"] = false return ds +} + +private fun Map.toCumulativeHourly(): Map { + fun hourKey(label: String): Int = label.substringBefore(":").toIntOrNull() ?: Int.MAX_VALUE + + val byHour = this.entries + .sortedBy { hourKey(it.key) } + .associate { it.key to it.value } + + val labels = (0..23).map { h -> h.toString().padStart(2, '0') + ":00" } + + var acc = 0 + val out = linkedMapOf() + labels.forEach { label -> + acc += byHour[label] ?: 0 + out[label] = acc + } + return out } \ No newline at end of file From 951f5960c95cec3bef24dd1d74e8a653bf643ea8 Mon Sep 17 00:00:00 2001 From: feragusper Date: Wed, 14 Jan 2026 12:37:14 +0100 Subject: [PATCH 3/3] feat(routing): implement hash-based navigation and refactor route handling --- .../com/feragusper/smokeanalytics/AppRoot.kt | 77 +++++++++---------- .../com/feragusper/smokeanalytics/WebRoute.kt | 26 +++++++ .../feragusper/smokeanalytics/WebScaffold.kt | 28 ++++--- 3 files changed, 76 insertions(+), 55 deletions(-) create mode 100644 apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebRoute.kt diff --git a/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/AppRoot.kt b/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/AppRoot.kt index 98918b3..805fc21 100644 --- a/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/AppRoot.kt +++ b/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/AppRoot.kt @@ -1,6 +1,7 @@ package com.feragusper.smokeanalytics import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -16,6 +17,8 @@ import com.feragusper.smokeanalytics.features.settings.presentation.web.Settings import com.feragusper.smokeanalytics.features.settings.presentation.web.createSettingsWebDependencies import com.feragusper.smokeanalytics.features.stats.presentation.web.StatsWebScreen import com.feragusper.smokeanalytics.features.stats.presentation.web.createStatsWebDependencies +import kotlinx.browser.window +import org.w3c.dom.events.Event /** * The root composable for the web application. @@ -24,13 +27,20 @@ import com.feragusper.smokeanalytics.features.stats.presentation.web.createStats */ @Composable fun AppRoot(graph: WebAppGraph) { - var tab by remember { mutableStateOf(WebTab.Home) } - var route by remember { mutableStateOf(WebRoute.Tabs) } + var route by remember { + mutableStateOf(parseRouteFromHash(window.location.hash)) + } + + DisposableEffect(Unit) { + val handler: (Event) -> Unit = { + route = parseRouteFromHash(window.location.hash) + } + window.addEventListener("hashchange", handler) + onDispose { window.removeEventListener("hashchange", handler) } + } val homeDeps = remember(graph) { - HomeWebDependencies( - homeProcessHolder = graph.homeProcessHolder, - ) + HomeWebDependencies(homeProcessHolder = graph.homeProcessHolder) } val historyDeps = remember(graph) { @@ -46,38 +56,35 @@ fun AppRoot(graph: WebAppGraph) { } when (route) { - WebRoute.Tabs -> { + WebRoute.Home, WebRoute.Stats, WebRoute.Settings -> { WebScaffold( - tab = tab, - onTabSelected = { tab = it }, + route = route, + onNavigate = ::navigateTo, ) { - when (tab) { - WebTab.Home -> HomeWebScreen( + when (route) { + WebRoute.Home -> HomeWebScreen( deps = homeDeps, - onNavigateToHistory = { route = WebRoute.History }, + onNavigateToHistory = { navigateTo(WebRoute.History) }, ) - WebTab.Stats -> { + WebRoute.Stats -> { val statsDeps = remember(graph) { - createStatsWebDependencies( - fetchSmokeStatsUseCase = graph.fetchSmokeStatsUseCase, - ) + createStatsWebDependencies(fetchSmokeStatsUseCase = graph.fetchSmokeStatsUseCase) } - StatsWebScreen( - deps = statsDeps - ) + StatsWebScreen(deps = statsDeps) } - WebTab.Settings -> { + WebRoute.Settings -> { val settingsDeps = remember(graph) { createSettingsWebDependencies( fetchSessionUseCase = graph.fetchSessionUseCase, signOutUseCase = graph.signOutUseCase, ) } - SettingsWebScreen(deps = settingsDeps) } + + else -> Unit } } } @@ -87,39 +94,29 @@ fun AppRoot(graph: WebAppGraph) { createAuthenticationWebDependencies( fetchSessionUseCase = graph.fetchSessionUseCase, signOutUseCase = graph.signOutUseCase, - signInWithGoogle = { /* no-op for now (handled by UI component) */ } + signInWithGoogle = { /* handled by UI component */ } ) } - AuthenticationWebScreen( deps = authDeps, - onLoggedIn = { - route = WebRoute.Tabs - tab = WebTab.Home - } + onLoggedIn = { navigateTo(WebRoute.Home) } ) } WebRoute.History -> { HistoryWebScreen( deps = historyDeps, - onNavigateUp = { - route = WebRoute.Tabs - tab = WebTab.Home - }, - onNavigateToAuth = { route = WebRoute.Auth }, + onNavigateUp = { navigateTo(WebRoute.Home) }, + onNavigateToAuth = { navigateTo(WebRoute.Auth) }, ) } } } -/** - * The dependency graph for the web application. - */ -private enum class WebRoute { Tabs, Auth, History } - -/** - * The tabs for the web application. - */ -enum class WebTab { Home, Stats, Settings } +private fun navigateTo(route: WebRoute) { + val target = route.toHash() + if (window.location.hash != target) { + window.location.hash = target + } +} \ No newline at end of file diff --git a/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebRoute.kt b/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebRoute.kt new file mode 100644 index 0000000..7781902 --- /dev/null +++ b/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebRoute.kt @@ -0,0 +1,26 @@ +package com.feragusper.smokeanalytics + +sealed class WebRoute { + data object Home : WebRoute() + data object Stats : WebRoute() + data object Settings : WebRoute() + data object History : WebRoute() + data object Auth : WebRoute() +} + +internal fun WebRoute.toHash(): String = when (this) { + WebRoute.Home -> "#/home" + WebRoute.Stats -> "#/stats" + WebRoute.Settings -> "#/settings" + WebRoute.History -> "#/history" + WebRoute.Auth -> "#/auth" +} + +internal fun parseRouteFromHash(hash: String): WebRoute = when (hash.removePrefix("#")) { + "/stats" -> WebRoute.Stats + "/settings" -> WebRoute.Settings + "/history" -> WebRoute.History + "/auth" -> WebRoute.Auth + "/home", "/", "" -> WebRoute.Home + else -> WebRoute.Home +} \ No newline at end of file diff --git a/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebScaffold.kt b/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebScaffold.kt index 33d51ab..2c54f99 100644 --- a/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebScaffold.kt +++ b/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebScaffold.kt @@ -7,8 +7,8 @@ import org.jetbrains.compose.web.dom.Text @Composable fun WebScaffold( - tab: WebTab, - onTabSelected: (WebTab) -> Unit, + route: WebRoute, + onNavigate: (WebRoute) -> Unit, content: @Composable () -> Unit, ) { Div(attrs = { classes(SmokeWebStyles.shell) }) { @@ -16,28 +16,26 @@ fun WebScaffold( Div(attrs = { classes(SmokeWebStyles.sidebarTitle) }) { Text("Smoke Analytics") } Div(attrs = { classes(SmokeWebStyles.navList) }) { - WebTab.entries.forEach { t -> + val items = listOf( + "Home" to WebRoute.Home, + "Stats" to WebRoute.Stats, + "Settings" to WebRoute.Settings, + ) + + items.forEach { (label, target) -> Div( attrs = { classes(SmokeWebStyles.navItem) - if (t == tab) classes(SmokeWebStyles.navItemActive) - onClick { onTabSelected(t) } + if (route == target) classes(SmokeWebStyles.navItemActive) + onClick { onNavigate(target) } } - ) { Text(t.label()) } + ) { Text(label) } } } } Div(attrs = { classes(SmokeWebStyles.main) }) { - Div(attrs = { classes(SmokeWebStyles.mainInner) }) { - content() - } + Div(attrs = { classes(SmokeWebStyles.mainInner) }) { content() } } } -} - -private fun WebTab.label(): String = when (this) { - WebTab.Home -> "Home" - WebTab.Stats -> "Stats" - WebTab.Settings -> "Settings" } \ No newline at end of file