diff --git a/budget-binder-common/src/commonMain/kotlin/de/hsfl/budgetBinder/common/Category.kt b/budget-binder-common/src/commonMain/kotlin/de/hsfl/budgetBinder/common/Category.kt index 876c2d74..9874a2ae 100644 --- a/budget-binder-common/src/commonMain/kotlin/de/hsfl/budgetBinder/common/Category.kt +++ b/budget-binder-common/src/commonMain/kotlin/de/hsfl/budgetBinder/common/Category.kt @@ -72,4 +72,12 @@ data class Category( val image: Image? = null, val budget: Float? = null ) + @Serializable + data class Nullable( + val id: Int?, + val name: String, + val color: String, + val image: Image, + val budget: Float + ) } \ No newline at end of file diff --git a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/di/DI.kt b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/di/DI.kt index 8c79f219..6ec87f71 100644 --- a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/di/DI.kt +++ b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/di/DI.kt @@ -18,13 +18,13 @@ import de.hsfl.budgetBinder.presentation.flow.RouterFlow import de.hsfl.budgetBinder.presentation.flow.UiEventSharedFlow import de.hsfl.budgetBinder.presentation.viewmodel.auth.login.LoginViewModel import de.hsfl.budgetBinder.presentation.viewmodel.auth.register.RegisterViewModel -import de.hsfl.budgetBinder.presentation.viewmodel.* import de.hsfl.budgetBinder.presentation.viewmodel.category.summary.CategorySummaryViewModel import de.hsfl.budgetBinder.presentation.viewmodel.category._CategoryViewModel import de.hsfl.budgetBinder.presentation.viewmodel.category.create.CategoryCreateViewModel import de.hsfl.budgetBinder.presentation.viewmodel.category.detail.CategoryDetailViewModel import de.hsfl.budgetBinder.presentation.viewmodel.category.edit.CategoryEditViewModel import de.hsfl.budgetBinder.presentation.viewmodel.dashboard.DashboardViewModel +import de.hsfl.budgetBinder.presentation.viewmodel.entry.EntryViewModel import de.hsfl.budgetBinder.presentation.viewmodel.navdrawer.NavDrawerViewModel import de.hsfl.budgetBinder.presentation.viewmodel.settings.SettingsEditServerUrlViewModel import de.hsfl.budgetBinder.presentation.viewmodel.settings.SettingsEditUserViewModel @@ -79,7 +79,7 @@ fun kodein(ktorEngine: HttpClientEngine) = DI { bindSingleton { StoreUserStateUseCase() } bindSingleton { StoreServerUrlUseCase() } bindSingleton { StoreDarkModeUseCase() } - bindSingleton { EntriesUseCases(instance(), instance(), instance(), instance(), instance()) } + bindSingleton { EntryUseCases(instance(), instance(), instance(), instance(), instance(), instance()) } bindSingleton { CategoriesUseCases(instance(), instance(), instance(), instance(), instance(), instance()) } bindSingleton { SettingsUseCases(instance(), instance(), instance()) } bindSingleton { AuthUseCases(instance(), instance(), instance()) } @@ -102,7 +102,7 @@ fun kodein(ktorEngine: HttpClientEngine) = DI { bindSingleton { CategoryDetailViewModel(instance(), instance(), instance()) } bindSingleton { CategoryEditViewModel(instance(), instance(), instance()) } bindSingleton { CategoryCreateViewModel(instance(), instance(), instance()) } - bindSingleton { EntryViewModel(instance(), instance()) } + bindSingleton { EntryViewModel(instance(), instance(), instance()) } bindSingleton { DashboardViewModel(instance(), instance(), instance(), instance()) } bindSingleton { NavDrawerViewModel(instance(), instance(), instance()) } } diff --git a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/domain/usecase/EntriesUseCases.kt b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/domain/usecase/EntryUseCases.kt similarity index 78% rename from budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/domain/usecase/EntriesUseCases.kt rename to budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/domain/usecase/EntryUseCases.kt index 0f914830..2ccc8b09 100644 --- a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/domain/usecase/EntriesUseCases.kt +++ b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/domain/usecase/EntryUseCases.kt @@ -1,7 +1,8 @@ package de.hsfl.budgetBinder.domain.usecase -data class EntriesUseCases( +data class EntryUseCases( val getAllEntriesUseCase: GetAllEntriesUseCase, + val getCategoryListUseCase : GetAllCategoriesUseCase, val getEntryByIdUseCase: GetEntryByIdUseCase, val createNewEntryUseCase: CreateNewEntryUseCase, val changeEntryByIdUseCase: ChangeEntryByIdUseCase, diff --git a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/Screen.kt b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/Screen.kt index c4a63e2b..e520016e 100644 --- a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/Screen.kt +++ b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/Screen.kt @@ -1,6 +1,4 @@ package de.hsfl.budgetBinder.presentation - - sealed class Screen { sealed class Welcome: Screen() { object Screen1: Welcome() @@ -21,29 +19,10 @@ sealed class Screen { } sealed class Entry: Screen() { data class Overview(val id: Int): Entry() - object Edit: Entry() + data class Edit(val id: Int): Entry() object Create: Entry() } object Login : Screen() object Register : Screen() object Dashboard : Screen() - - @Deprecated(message = "use sealed class Welcome") - object _Welcome : Screen() - @Deprecated(message = "use sealed class Settings") - object _Settings : Screen() - @Deprecated(message = "use sealed class Settings") - object SettingsChangeUserData : Screen() - @Deprecated(message = "use sealed class Category") - object CategorySummary : Screen() - @Deprecated(message = "use sealed class Category") - object CategoryEdit : Screen() - @Deprecated(message = "use sealed class Category") - object CategoryCreate : Screen() - @Deprecated(message = "use sealed class Category") - object CategoryCreateOnRegister : Screen() - @Deprecated(message = "use sealed class Entry") - object EntryEdit : Screen() - @Deprecated(message = "use sealed class Entry") - object EntryCreate : Screen() } diff --git a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/EntryViewModel.kt b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/EntryViewModel.kt index 23ab5a8c..b67d908f 100644 --- a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/EntryViewModel.kt +++ b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/EntryViewModel.kt @@ -10,14 +10,14 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.* class EntryViewModel( - private val entriesUseCases: EntriesUseCases, + private val entryUseCases: EntryUseCases, private val scope: CoroutineScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) ) { private val _state = MutableStateFlow(UiState.Empty) val state: StateFlow = _state fun getAllEntries() { - entriesUseCases.getAllEntriesUseCase().onEach { + entryUseCases.getAllEntriesUseCase().onEach { when (it) { is DataResponse.Success -> _state.value = UiState.Success(it.data) is DataResponse.Error -> _state.value = UiState.Error(it.error!!.message) @@ -28,7 +28,7 @@ class EntryViewModel( } fun getEntryById(id: Int) { - entriesUseCases.getEntryByIdUseCase(id).onEach { + entryUseCases.getEntryByIdUseCase(id).onEach { when (it) { is DataResponse.Success -> _state.value = UiState.Success(it.data) is DataResponse.Error -> _state.value = UiState.Error(it.error!!.message) @@ -39,7 +39,7 @@ class EntryViewModel( } fun createEntry(entry: Entry.In) { - entriesUseCases.createNewEntryUseCase(entry).onEach { + entryUseCases.createNewEntryUseCase(entry).onEach { when (it) { is DataResponse.Success -> _state.value = UiState.Success(it.data) is DataResponse.Error -> _state.value = UiState.Error(it.error!!.message) @@ -50,7 +50,7 @@ class EntryViewModel( } fun changeEntry(entry: Entry.Patch, id: Int) { - entriesUseCases.changeEntryByIdUseCase(entry, id).onEach { + entryUseCases.changeEntryByIdUseCase(entry, id).onEach { when (it) { is DataResponse.Success -> _state.value = UiState.Success(it.data) is DataResponse.Error -> _state.value = UiState.Error(it.error!!.message) @@ -61,7 +61,7 @@ class EntryViewModel( } fun removeEntry(id: Int) { - entriesUseCases.deleteEntryByIdUseCase(id).onEach { + entryUseCases.deleteEntryByIdUseCase(id).onEach { when (it) { is DataResponse.Success -> _state.value = UiState.Success(it.data) is DataResponse.Error -> _state.value = UiState.Error(it.error!!.message) diff --git a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/auth/login/LoginViewModel.kt b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/auth/login/LoginViewModel.kt index 2d4799ee..a333b37f 100644 --- a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/auth/login/LoginViewModel.kt +++ b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/auth/login/LoginViewModel.kt @@ -55,6 +55,9 @@ class LoginViewModel( private fun validateInput() { if (validateEmail(email = emailText.value.email)) { + //TODO: Check what frontend this is opened from. + // Web -> super.login(...), + // everyone else -> toggleDialog() toggleDialog() } else { _emailText.value = emailText.value.copy(emailValid = false) diff --git a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/category/edit/CategoryEditViewModel.kt b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/category/edit/CategoryEditViewModel.kt index 5327dc0a..27053150 100644 --- a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/category/edit/CategoryEditViewModel.kt +++ b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/category/edit/CategoryEditViewModel.kt @@ -50,13 +50,13 @@ class CategoryEditViewModel( onSuccess = { routerFlow.navigateTo(Screen.Category.Summary) } ) is CategoryEditEvent.LifeCycle -> event.value.handleLifeCycle( - onLaunch = { initSateFlows() }, + onLaunch = { initStateFlows() }, onDispose = { resetStateFlows() } ) } } - private fun initSateFlows() { + private fun initStateFlows() { super.updateCurrentCategoryId() super.getById(id = currentCategoryId, onSuccess = { category -> _categoryState.value = category diff --git a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/entry/EntryEvent.kt b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/entry/EntryEvent.kt new file mode 100644 index 00000000..fbdda0dd --- /dev/null +++ b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/entry/EntryEvent.kt @@ -0,0 +1,27 @@ +package de.hsfl.budgetBinder.presentation.viewmodel.entry + +import de.hsfl.budgetBinder.presentation.event.LifecycleEvent + +// View is not allowed to declare what should be done, only notify what has happened, names are assigned as such + +sealed class EntryEvent { + //User-made Data Input + data class EnteredName(val value: String) : EntryEvent() + data class EnteredAmount(val value: Float) : EntryEvent() + object EnteredRepeat : EntryEvent() + data class EnteredCategoryID(val value: Int?) : EntryEvent() + object EnteredAmountSign : EntryEvent() + + //Action + object OnCreateEntry : EntryEvent() + object OnEditEntry : EntryEvent() + object OnDeleteEntry : EntryEvent() + object OnDeleteDialogConfirm : EntryEvent() + object OnDeleteDialogDismiss : EntryEvent() + object OnCancel: EntryEvent() + + + //LifeCycle + data class LifeCycle(val value: LifecycleEvent): EntryEvent() + +} diff --git a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/entry/EntryInputState.kt b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/entry/EntryInputState.kt new file mode 100644 index 00000000..db25b34e --- /dev/null +++ b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/entry/EntryInputState.kt @@ -0,0 +1,9 @@ +package de.hsfl.budgetBinder.presentation.viewmodel.entry + +data class EntryInputState( + val name: String = "", + val amount: Float = 0f, + val repeat: Boolean = false, + val categoryID: Int? = null, + val amountSign: Boolean = false +) diff --git a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/entry/EntryState.kt b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/entry/EntryState.kt new file mode 100644 index 00000000..b3486a6a --- /dev/null +++ b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/entry/EntryState.kt @@ -0,0 +1,9 @@ +package de.hsfl.budgetBinder.presentation.viewmodel.entry + +import de.hsfl.budgetBinder.common.Category +import de.hsfl.budgetBinder.common.Entry + +data class EntryState( + val selectedEntry: Entry = Entry(0,"",0f,false,null), + val categoryList: List = listOf(Category(0,"No category","ffffff",Category.Image.DEFAULT,0f)) +) diff --git a/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/entry/EntryViewModel.kt b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/entry/EntryViewModel.kt new file mode 100644 index 00000000..82ca8bdb --- /dev/null +++ b/budget-binder-multiplatform-app/src/commonMain/kotlin/de/hsfl/budgetBinder/presentation/viewmodel/entry/EntryViewModel.kt @@ -0,0 +1,184 @@ +package de.hsfl.budgetBinder.presentation.viewmodel.entry + +import de.hsfl.budgetBinder.common.Category +import de.hsfl.budgetBinder.common.DataResponse +import de.hsfl.budgetBinder.common.Entry +import de.hsfl.budgetBinder.domain.usecase.* +import de.hsfl.budgetBinder.presentation.Screen +import de.hsfl.budgetBinder.presentation.event.UiEvent +import de.hsfl.budgetBinder.presentation.event.handleLifeCycle +import de.hsfl.budgetBinder.presentation.flow.DataFlow +import de.hsfl.budgetBinder.presentation.flow.RouterFlow +import de.hsfl.budgetBinder.presentation.flow.UiEventSharedFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlin.math.absoluteValue + +class EntryViewModel( + private val entryUseCases: EntryUseCases, + private val routerFlow: RouterFlow, + private val scope: CoroutineScope +) { + /* *** Variables *** */ + + // ---- Data Input Variables ---- + private val _nameText = MutableStateFlow(EntryInputState().name) + val nameText: StateFlow = _nameText + + private val _amountText = MutableStateFlow(EntryInputState().amount) + val amountText: StateFlow = _amountText + + private val _repeatState = MutableStateFlow(EntryInputState().repeat) + val repeatState: StateFlow = _repeatState + + private val _categoryIDState = MutableStateFlow(EntryInputState().categoryID) + val categoryIDState: StateFlow = _categoryIDState + + private val _amountSignState = MutableStateFlow(EntryInputState().amountSign) + val amountSignState: StateFlow = _amountSignState + + private val _selectedEntryState = MutableStateFlow(EntryState().selectedEntry) + val selectedEntryState: StateFlow = _selectedEntryState + + private val _categoryListState = MutableStateFlow(EntryState().categoryList) + val categoryListState: StateFlow> = _categoryListState + + // --- Default ViewModel Variables ---- + private val _dialogState = MutableStateFlow(false) + val dialogState: StateFlow = _dialogState + val eventFlow = UiEventSharedFlow.mutableEventFlow + // ---- + + + /* *** Event Handling *** */ + fun onEvent(event: EntryEvent) { + when (event) { + is EntryEvent.EnteredName -> _nameText.value = event.value + is EntryEvent.EnteredAmount -> _amountText.value = event.value + is EntryEvent.EnteredRepeat -> _repeatState.value = !repeatState.value + is EntryEvent.EnteredCategoryID -> _categoryIDState.value = event.value + is EntryEvent.EnteredAmountSign -> _amountSignState.value = !amountSignState.value + is EntryEvent.OnCreateEntry -> { + when (routerFlow.state.value) { + is Screen.Entry.Create -> create( + Entry.In( + nameText.value, + buildAmount(), + repeatState.value, + categoryIDState.value + ) + ) + else -> routerFlow.navigateTo(Screen.Entry.Create) + } + } + is EntryEvent.OnEditEntry -> + when (routerFlow.state.value) { + is Screen.Entry.Edit -> update( + Entry.Patch( + nameText.value, + buildAmount(), + repeatState.value, + Entry.Category(categoryIDState.value) + ), selectedEntryState.value.id + ) + else -> { + routerFlow.navigateTo(Screen.Entry.Edit(selectedEntryState.value.id)) + } //using ID seems... unnecessary?} + } + is EntryEvent.OnDeleteEntry -> _dialogState.value = true + is EntryEvent.OnDeleteDialogConfirm -> delete(selectedEntryState.value.id) + is EntryEvent.OnDeleteDialogDismiss -> _dialogState.value = false + is EntryEvent.OnCancel -> routerFlow.navigateTo(Screen.Dashboard) + is EntryEvent.LifeCycle -> event.value.handleLifeCycle( + onLaunch = { + getCategoryList() + when (routerFlow.state.value) { + is Screen.Entry.Overview -> getById((routerFlow.state.value as Screen.Entry.Overview).id) + is Screen.Entry.Edit -> getById((routerFlow.state.value as Screen.Entry.Edit).id) + else -> { + } + } + }, + onDispose = { + resetFlows() + }) + } + } + + + /* *** Use Case usages *** */ + private fun getById(id: Int) = scope.launch { + entryUseCases.getEntryByIdUseCase(id).collect { + it.handleDataResponse(routerFlow = routerFlow, onSuccess = { entry -> + _selectedEntryState.value = entry + _nameText.value = _selectedEntryState.value.name + _amountText.value = _selectedEntryState.value.amount.absoluteValue + _amountSignState.value = _selectedEntryState.value.amount >= 0 + _repeatState.value = _selectedEntryState.value.repeat + _categoryIDState.value = _selectedEntryState.value.category_id + }) + } + } + + private fun getCategoryList() = scope.launch { + entryUseCases.getCategoryListUseCase().collect { + it.handleDataResponse>( + routerFlow = routerFlow, onSuccess = { cl -> _categoryListState.value = cl }) + } + } + + private fun create(entry: Entry.In) = scope.launch { + entryUseCases.createNewEntryUseCase(entry).collect { + it.handleDataResponse( + routerFlow = routerFlow, onSuccess = { + routerFlow.navigateTo(Screen.Dashboard) + eventFlow.emit(UiEvent.ShowSuccess("Entry successfully created")) + }) + } + } + + private fun update(entry: Entry.Patch, id: Int) = scope.launch { + entryUseCases.changeEntryByIdUseCase(entry, id).collect { + it.handleDataResponse( + routerFlow = routerFlow, onSuccess = { + routerFlow.navigateTo(Screen.Dashboard) + eventFlow.emit(UiEvent.ShowSuccess("Entry successfully updated")) + }) + } + } + + private fun delete(id: Int) = scope.launch { + entryUseCases.deleteEntryByIdUseCase(id).collect { + it.handleDataResponse( + routerFlow = routerFlow, onSuccess = { + routerFlow.navigateTo(Screen.Dashboard) + eventFlow.emit(UiEvent.ShowSuccess("Entry successfully deleted")) + }) + } + } + + + /** + * Negates amount if amountSign is false + * */ + private fun buildAmount(): Float { + return if (_amountSignState.value) { + _amountText.value + } else { + _amountText.value * -1 + } + } + + /** + * Resets Data Input Variables + */ + private fun resetFlows() { + _nameText.value = EntryInputState().name + _amountText.value = EntryInputState().amount + _amountSignState.value = EntryInputState().amountSign + _repeatState.value = EntryInputState().repeat + _categoryIDState.value = EntryInputState().categoryID + } + +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/common/Constants.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/common/Constants.kt index 832ff56f..f49b4ef7 100644 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/common/Constants.kt +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/common/Constants.kt @@ -5,5 +5,4 @@ actual object Constants { //Set to nothing because it hosts itself (so we don't have to deal with CORS) //for local server: http://localhost:8080 //for online server: https://bb-server.fpcloud.de - val DEFAULT_CATEGORY = (Category(0, "", "FFFFFF", Category.Image.DEFAULT, 0f)) -} \ No newline at end of file +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/CategoryComposables.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/CategoryComposables.kt new file mode 100644 index 00000000..d2f57e7d --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/CategoryComposables.kt @@ -0,0 +1,231 @@ +package de.hsfl.budgetBinder.compose + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.common.Category +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import de.hsfl.budgetBinder.presentation.CategoryImageToIcon +import org.jetbrains.compose.web.ExperimentalComposeWebSvgApi +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.* +import org.jetbrains.compose.web.svg.Circle +import org.jetbrains.compose.web.svg.Rect +import org.jetbrains.compose.web.svg.Svg + +@OptIn(ExperimentalComposeWebSvgApi::class) +@Composable +fun BudgetBar( + focusedCategory: Category, + totalSpendBudget: Float, + totalBudget: Float, +) { + //width and height are for aspect ratio - tries to fill out wherever its in, so its more like + val width = 20 + val height = 2 + + H1(attrs = { classes("mdc-typography--headline4", AppStylesheet.flexContainer) }) { + CategoryImageToIcon(focusedCategory.image) + Text("${focusedCategory.name} - Budget") + } + + Div { + if (totalSpendBudget <= totalBudget && totalBudget > 0) { //Normal not Spent Budget + //Money Text + MoneyTextDiv { + Div(attrs = { + classes("mdc-typography--headline5") + }) { Text(totalSpendBudget.toString() + "€") } + Div(attrs = { classes("mdc-typography--headline5") }) { Text(totalBudget.toString() + "€") } + } + Svg(viewBox = "0 0 $width $height") { + Rect(x = 0, y = 0, width = width, height = height, { + attr("fill", Color.lightgray.toString()) + }) + if (0 < totalSpendBudget) // If there is used budget, draw it + Rect( + x = 0, + y = 0, + width = totalSpendBudget / totalBudget * width, + height = height, + { + attr("fill", "#" + focusedCategory.color) + }) + } + } else if (totalSpendBudget > totalBudget && totalBudget > 0) { //Over Budget + MoneyTextDiv { + Div(attrs = { classes("mdc-typography--headline5") }) { Text("Budget limit for " + focusedCategory.name + " reached! " + totalSpendBudget.toString() + "€ of " + totalBudget.toString() + "€ Budget spent") } + } + Svg(viewBox = "0 0 $width $height") { + Rect(x = 0, y = 0, width = width, height = height, { + attr("fill", "#b00020") + }) + } + } else if (totalBudget <= 0f) { //No Category View or other unpredictable case (or no categories, overall screen) + MoneyTextDiv { + Div(attrs = { classes("mdc-typography--headline5") }) { Text(totalSpendBudget.toString() + "€ spent") } + } + Svg(viewBox = "0 0 $width $height") { + Rect(x = 0, y = 0, width = width, height = height, { + attr("fill", "#" + focusedCategory.color) + }) + } + } + } +} + +@Composable +fun MoneyTextDiv(content: @Composable () -> Unit) { + Div(attrs = { + style { + display(DisplayStyle.Flex) + justifyContent(JustifyContent("space-between")) + } + }) { + content() + } +} + +@OptIn(ExperimentalComposeWebSvgApi::class) +@Composable +fun CategoryList( + categoryList: List, + onClicked: (Int) -> Unit, +) { + Div { + for (category in categoryList) + CategoryElement(category, onClicked = onClicked) + } +} + +@OptIn(ExperimentalComposeWebSvgApi::class) +@Composable +fun CategoryElement(category: Category, onClicked: (Int) -> Unit) { + Div(attrs = { + classes("mdc-card", AppStylesheet.card) + onClick { onClicked(category.id) } + } + ) { + Div( + attrs = { + classes(AppStylesheet.categoryListElement, AppStylesheet.flexContainer) + } + ) { + Div( + attrs = { + classes(AppStylesheet.imageFlexContainer) + } + ) { + CategoryImageToIcon(category.image) + } + Div( + attrs = { + classes(AppStylesheet.categoryListElementText) + } + ) { + Div { + Div(attrs = { + classes("mdc-typography--headline4", AppStylesheet.text) + }) { Text(category.name) } + Div(attrs = { + classes("mdc-typography--headline6", AppStylesheet.text) + }) { Text("Budget: ${category.budget}€") } + } + } + Div(attrs = { + classes(AppStylesheet.imageFlexContainer) + } + ) { + Svg(viewBox = "0 0 1 1") {//For aspect ratio - tries to fill out wherever it is in + Circle(cx = 0.5, cy = 0.5, r = 0.5, { + attr("fill", "#${category.color}") + }) + } + } + } + } +} + +@OptIn(ExperimentalComposeWebSvgApi::class) +@Composable +fun CategoryDetailed( + category: Category, + onEditButton: () -> Unit, + onDeleteButton: () -> Unit, + onBackButton: () -> Unit +) { + var deleteDialog by remember { mutableStateOf(false) } + Div(attrs = { + classes("mdc-card", AppStylesheet.card) + } + ) { + if (deleteDialog) { + DeleteDialog( + false, + { onDeleteButton() }, + { deleteDialog = false }) { Text("Delete Category?") } + } + Div( + attrs = { + classes(AppStylesheet.categoryListElement, AppStylesheet.flexContainer) + } + ) { + Div( + attrs = { + classes(AppStylesheet.imageFlexContainer) + } + ) { + CategoryImageToIcon(category.image) + } + Div( + attrs = { + classes(AppStylesheet.categoryListElementText) + } + ) { + Div { + Div(attrs = { + classes("mdc-typography--headline4", AppStylesheet.text) + }) { Text(category.name) } + Div(attrs = { + classes("mdc-typography--headline6", AppStylesheet.text) + }) { Text("Budget: ${category.budget}€") } + } + } + Div(attrs = { + classes(AppStylesheet.imageFlexContainer) + } + ) { + Svg(viewBox = "0 0 1 1") {//For aspect ratio - tries to fill out wherever it is in + Circle(cx = 0.5, cy = 0.5, r = 0.5, { + attr("fill", "#${category.color}") + }) + } + } + } + Div( + attrs = { + classes(AppStylesheet.flexContainer) + } + ) { + Button(attrs = { + classes("mdc-button", "mdc-button--raised", AppStylesheet.buttonOverview) + onClick { onBackButton() } + } + ) { + Span(attrs = { classes("mdc-button__label") } + ) { Text("Go back") } + } + Button(attrs = { + classes("mdc-button", "mdc-button--raised", AppStylesheet.buttonOverview) + onClick { onEditButton() } + }) { + Text("Edit Category") + } + Button(attrs = { + classes("mdc-button", "mdc-button--raised", AppStylesheet.buttonOverview) + onClick { deleteDialog = !deleteDialog } + style { backgroundColor(Color("#b00020")) } + }) { + Text("Delete Category") + } + } + } +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/Composables.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/Composables.kt index d31dcabe..3dd2ebde 100644 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/Composables.kt +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/Composables.kt @@ -1,32 +1,41 @@ package de.hsfl.budgetBinder.compose +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.common.Category import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import de.hsfl.budgetBinder.compose.theme.AppStylesheet -import de.hsfl.budgetBinder.presentation.Screen +import de.hsfl.budgetBinder.presentation.CategoryImageToIcon +import org.jetbrains.compose.web.attributes.ButtonType +import org.jetbrains.compose.web.attributes.type import org.jetbrains.compose.web.css.* import org.jetbrains.compose.web.dom.* /*Main Container for every mayor layout*/ @Composable -fun MainFlexContainer(content: @Composable () -> Unit){ - Div ( +fun MainFlexContainer(content: @Composable () -> Unit) { + Div( attrs = { classes("mdc-top-app-bar--fixed-adjust", AppStylesheet.flexContainer) } - ){ - Div (attrs = { classes(AppStylesheet.pufferFlexContainer) }) - Div (attrs = { classes(AppStylesheet.contentFlexContainer)}) + ) { + Div(attrs = { classes(AppStylesheet.pufferFlexContainer) }) + Div(attrs = { classes(AppStylesheet.contentFlexContainer) }) { - content() + Div(attrs = { + classes("mdc-card", AppStylesheet.card) + } + ) { + content() + } } - Div (attrs = { classes(AppStylesheet.pufferFlexContainer)}) + Div(attrs = { classes(AppStylesheet.pufferFlexContainer) }) } } +@Deprecated("Use new NavBar instead! just write NavBar{} over your MainFlexContainer!") @Composable -fun topBarMain(logoButton: @Composable () -> Unit, navButtons: @Composable () -> Unit){ +fun topBarMain(logoButton: @Composable () -> Unit, navButtons: @Composable () -> Unit) { Header( attrs = { classes("mdc-top-app-bar") @@ -61,3 +70,280 @@ fun topBarMain(logoButton: @Composable () -> Unit, navButtons: @Composable () -> } } } + + +///* Gives a material icon based on the icon name*/// +@Composable +fun Icon(icon_name: String) { + Span( + attrs = { + classes("material-icons") + style { + width(24.px) + height(24.px) + } + } + ) { Text(icon_name) } +} + + +// Snackbar that shows msg +@Composable +fun FeedbackSnackbar(msg: String, hidden: Boolean = false, onDismiss: () -> Unit) { + Aside( + attrs = { + when (hidden) { + false -> classes("mdc-snackbar", "mdc-snackbar--open") + true -> classes("mdc-snackbar") + } + onClick { + onDismiss() + } + }) { + Div(attrs = { + classes("mdc-snackbar__surface") + attr(attr = "role", value = "status") + attr(attr = "aria-relevant", value = "additions") + + }) { + Div(attrs = { + classes("mdc-snackbar__label") + attr(attr = "aria-atomic", value = "false") + }) { + Text(msg) + } + Div(attrs = { + classes("mdc-snackbar__actions") + attr(attr = "aria-atomic", value = "true") + }) { + Button(attrs = { + classes("mdc-button", "mdc-snackbar__action") + onClick { onDismiss() } + }) { + Div(attrs = { + classes("mdc-button__ripple") + }) + Span(attrs = { classes("mdc-button__label") }) + { Text("Dismiss") } + } + } + } + } +} + + +@Composable +fun CategoryImagesToImageList( + inputImage: Category.Image, + onClick: (Category.Image) -> Unit +) { + Div( + attrs = { + classes("mdc-card", AppStylesheet.card) + } + ) { + Ul( + attrs = { + classes("mdc-image-list", AppStylesheet.categoryImageList) + } + ) { + for (image in Category.Image.values()) { + Li( + attrs = { + classes("mdc-image-list__item") + } + ) { + Div( + attrs = { + if (inputImage == image) + classes( + "mdc-image-list__image-aspect-container", + "mdc-icon-button", + "mdc-button--raised" + ) + else classes( + "mdc-image-list__image-aspect-container", + "mdc-icon-button" + ) + onClick { onClick(image) } + } + ) { + CategoryImageToIcon(image) + } + } + } + } + } +} + + +@Composable +fun DeleteDialog( + hidden: Boolean, + buttonAction: () -> Unit, + resetDialog: () -> Unit, + content: @Composable () -> Unit +) { + var hiddenValue by remember { mutableStateOf(hidden) } + Div( + attrs = { + when (hiddenValue) { + false -> classes("mdc-dialog", "mdc-dialog--open") + true -> classes("mdc-dialog") + } + } + ) { + Div( + attrs = { + classes("mdc-dialog__container") + } + ) { + Div( + attrs = { + classes("mdc-dialog__surface") + attr("role", "alertdialog") + attr("aria-modal", "true") + attr("aria-labelledby", "my-dialog-title") + attr("aria-describedby", "my-dialog-content") + } + ) { + Div( + attrs = { + classes("mdc-dialog__content") + id("my-dialog-content") + } + ) { + content() //Text in Dialog + } + Div( + attrs = { + classes("mdc-dialog__actions") + } + ) { + Button( + attrs = { + classes("mdc-button", "mdc-dialog__button") + attr("data-mdc-dialog-action", "cancel") + onClick { + hiddenValue = true + resetDialog() + } + } + ) { + Div( + attrs = { + classes("mdc-button__ripple") + } + ) { + + } + Span( + attrs = { + classes("mdc-button__label") + } + ) { + Text("Cancel") + } + } + Button( + attrs = { + classes("mdc-button", "mdc-dialog__button") + attr("data-mdc-dialog-action", "accept") + onClick { + buttonAction() + hiddenValue = true + resetDialog() + } + } + ) { + Div( + attrs = { + classes("mdc-button__ripple") + } + ) { + + } + Span( + attrs = { + classes("mdc-button__label") + } + ) { + Text("OK") + } + } + } + } + } + Div( + attrs = { + classes("mdc-dialog__scrim") + } + ) { + + } + } +} + +@Composable +fun ChooseCategoryMenu( + categoryList: List, + selectedCategory: Int?, + getCategoryId: (Int?) -> Unit +) { + var categoryListWN = listOf(Category.Nullable(null, "No Category", "ffffff", Category.Image.DEFAULT, 0f)) + for (category in categoryList) { + categoryListWN = categoryListWN + (Category.Nullable(category.id, category.name, category.color, category.image, category.budget)) + } + console.log(categoryList) + var choseCat = categoryListWN[0] + + for (category in categoryListWN) { + if (category.id == selectedCategory) { + choseCat = category + break + } + } + + var chosenCategory by remember { + mutableStateOf(choseCat) + } + console.log(chosenCategory) + + + var showList by remember { mutableStateOf(false) } + + Button(attrs = { + classes("mdc-button", "mdc-dialog__button") + onClick { showList = !showList } + type(ButtonType.Button) + }) { + Div(attrs = { + when (showList) { + true -> classes("mdc-menu", "mdc-menu-surface", "mdc-menu-surface--open") + false -> classes("mdc-menu", "mdc-menu-surface") + } + }) { + Ul(attrs = { + classes("mdc-list") + attr("role", "menu") + attr("aria-hidden", "true") + attr("aria-orientation", "vertical") + attr("tabindex", "-1") + }) { + for (category in categoryListWN) { + Li(attrs = { + classes("mdc-list-item") + attr("role", "menuitem") + onClick { chosenCategory = category; getCategoryId(category.id) } + }) { + Span(attrs = { classes("mdc-list-item__ripple") }) { } + Span(attrs = { classes(AppStylesheet.moneyText) }) { Text(category.name) } + } + } + } + } + Text(chosenCategory.name) + } +} + + diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/NavBar.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/NavBar.kt new file mode 100644 index 00000000..46a9f651 --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/NavBar.kt @@ -0,0 +1,131 @@ +package de.hsfl.budgetBinder.compose + +import androidx.compose.runtime.Composable +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import de.hsfl.budgetBinder.presentation.viewmodel.navdrawer.NavDrawerEvent +import de.hsfl.budgetBinder.presentation.viewmodel.navdrawer.NavDrawerViewModel +import di +import org.jetbrains.compose.web.css.Color +import org.jetbrains.compose.web.css.backgroundColor +import org.jetbrains.compose.web.dom.* +import org.kodein.di.instance + +//Top Navigation Bar +@Composable +fun NavBar(content: @Composable () -> Unit) { + val viewModel: NavDrawerViewModel by di.instance() + Header( + attrs = { + classes("mdc-top-app-bar") + } + ) { + Div( + attrs = { + classes("mdc-top-app-bar__row") + } + ) { + Section( + attrs = { + classes("mdc-top-app-bar__section", "mdc-top-app-bar__section--align-start") + } + ) { + Img( + src = "images/Logo.png", alt = "Logo", attrs = { + classes("mdc-icon-button", AppStylesheet.image) + onClick { viewModel.onEvent(NavDrawerEvent.OnDashboard) } + } + ) + Span( + attrs = { + classes("mdc-top-app-bar__title") + } + ) { + Text("Budget-Binder") + } + } + Section( + attrs = { + classes("mdc-top-app-bar__section", "mdc-top-app-bar__section--align-end") + } + ) { + Button( + attrs = { + classes( + "mdc-button", + "mdc-button--raised", + "mdc-top-app-bar__navigation-icon", + AppStylesheet.marginRight + ) + onClick { viewModel.onEvent(NavDrawerEvent.OnCreateEntry) } + } + ) { + Span( + attrs = { + classes("mdc-button__label") + } + ) { + Text("New Entry") + } + } + Button( + attrs = { + classes( + "mdc-button", + "mdc-button--raised", + "mdc-top-app-bar__navigation-icon", + AppStylesheet.marginRight + ) + onClick { viewModel.onEvent(NavDrawerEvent.OnCategory) } + } + ) { + Span( + attrs = { + classes("mdc-button__label") + } + ) { + Text("Categories") + } + } + Button( + attrs = { + classes( + "mdc-button", + "mdc-button--raised", + "mdc-top-app-bar__navigation-icon", + AppStylesheet.marginRight + ) + onClick { viewModel.onEvent(NavDrawerEvent.OnSettings) } + } + ) { + Span( + attrs = { + classes("mdc-button__label") + } + ) { + Text("Settings") + } + } + Button( + attrs = { + classes( + "mdc-button", + "mdc-button--raised", + "mdc-top-app-bar__navigation-icon" + ) + onClick { viewModel.onEvent(NavDrawerEvent.OnLogout) } + style { backgroundColor(Color("#b00020")) } + } + ) { + Span( + attrs = { + classes("mdc-button__label") + } + ) { + Text("Logout") + } + } + } + } + } + content() +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/Router.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/Router.kt index 3f09cfb0..d7b79080 100644 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/Router.kt +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/Router.kt @@ -1,24 +1,40 @@ package de.hsfl.budgetBinder.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import de.hsfl.budgetBinder.compose.category.CategoryComponent -import de.hsfl.budgetBinder.compose.dashboard.DashboardComponent -import de.hsfl.budgetBinder.compose.entry.EntryComponent -import de.hsfl.budgetBinder.compose.login.LoginComponent -import de.hsfl.budgetBinder.compose.register.RegisterComponent -import de.hsfl.budgetBinder.compose.settings.SettingsComponent +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.rememberCoroutineScope import de.hsfl.budgetBinder.presentation.Screen +import de.hsfl.budgetBinder.presentation.flow.RouterFlow +import de.hsfl.budgetBinder.screens.category.CategoryComponent +import de.hsfl.budgetBinder.screens.dashboard.DashboardComponent +import de.hsfl.budgetBinder.screens.entry.EntryComponent +import de.hsfl.budgetBinder.screens.login.LoginComponent +import de.hsfl.budgetBinder.screens.register.RegisterComponent +import de.hsfl.budgetBinder.screens.settings.SettingsComponent +import di +import org.jetbrains.compose.web.dom.Text +import org.kodein.di.instance @Composable -fun Router(screenState: MutableState) { +fun Router() { + val scope = rememberCoroutineScope() + val routerFlow: RouterFlow by di.instance() + val screenState = routerFlow.state.collectAsState(scope.coroutineContext) + console.log("Router Screen is:") + console.log(screenState.value) when (screenState.value) { - Screen._Welcome -> {} - Screen.Register -> RegisterComponent(screenState = screenState) - Screen.Login -> LoginComponent(screenState = screenState) - Screen.Dashboard -> DashboardComponent(screenState = screenState) - Screen._Settings, Screen.SettingsChangeUserData -> SettingsComponent(screenState = screenState) - Screen.CategorySummary,Screen.CategoryEdit,Screen.CategoryCreate, Screen.CategoryCreateOnRegister -> CategoryComponent(screenState = screenState) - Screen.EntryCreate, Screen.EntryEdit -> EntryComponent(screenState = screenState) + is Screen.Welcome -> {} + is Screen.Register -> RegisterComponent() + is Screen.Login -> LoginComponent() + is Screen.Dashboard -> DashboardComponent() + is Screen.Settings -> SettingsComponent() + is Screen.Entry -> EntryComponent() + is Screen.Category.Detail -> CategoryComponent() //To avoid weird bug where it doesn't refresh itself? I don't understand either ... + is Screen.Category.Edit -> CategoryComponent() //Okay this seems to be necessary or CategoryComponent won't refresh, so no 'is Screen.Category -> ...' + is Screen.Category.Create -> CategoryComponent() + is Screen.Category.Summary -> CategoryComponent() + else -> { + Text("No known Screen! Check if the screen you're trying to reach is in the ScreenRouter") + } } -} \ No newline at end of file +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategoryComponent.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategoryComponent.kt deleted file mode 100644 index 8d483556..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategoryComponent.kt +++ /dev/null @@ -1,115 +0,0 @@ -package de.hsfl.budgetBinder.compose.category - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.common.Category -import de.hsfl.budgetBinder.common.Constants.DEFAULT_CATEGORY -import de.hsfl.budgetBinder.common.Entry -import de.hsfl.budgetBinder.presentation.Screen -import de.hsfl.budgetBinder.presentation.viewmodel.category._CategoryViewModel -import di -import org.jetbrains.compose.web.ExperimentalComposeWebSvgApi -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.dom.* -import org.jetbrains.compose.web.svg.Rect -import org.jetbrains.compose.web.svg.Svg -import org.kodein.di.instance - -@Composable -fun CategoryComponent(screenState: MutableState) { - // Old => CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) - // Use rememberCoroutineScope() - val scope = rememberCoroutineScope() - - // Don't do this like here, use Kodein - /* val di = localDI() - val getAllCategoriesUseCase: GetAllCategoriesUseCase by di.instance() - val createCategoryUseCase: CreateCategoryUseCase by di.instance() - val getCategoryByIdUseCase: GetCategoryByIdUseCase by di.instance() - val changeCategoryByIdUseCase: ChangeCategoryByIdUseCase by di.instance() - val deleteCategoryByIdUseCase: DeleteCategoryByIdUseCase by di.instance() - val getAllEntriesByCategoryUseCase: GetAllEntriesByCategoryUseCase by di.instance() - val categoryViewModel = CategoryViewModel(getAllCategoriesUseCase, getCategoryByIdUseCase,createCategoryUseCase, changeCategoryByIdUseCase, deleteCategoryByIdUseCase, getAllEntriesByCategoryUseCase, scope) - val viewState = categoryViewModel.state.collectAsState(scope)*/ - - val viewModel: _CategoryViewModel by di.instance() - val viewState = viewModel.state.collectAsState(scope.coroutineContext) - - when (screenState.value) { - Screen.CategoryCreate -> CategoryCreateView( - state = viewState, - onBackButton = { screenState.value = Screen.CategorySummary} - ) - Screen.CategorySummary -> CategorySummaryView( - state = viewState, - onBackButton = { screenState.value = Screen.Dashboard}, - onEditButton = { screenState.value = Screen.CategoryEdit}, - onCategoryCreateButton = { screenState.value = Screen.CategoryCreate}, - ) - Screen.CategoryEdit -> CategoryEditView( - state = viewState, - onBackButton = { screenState.value = Screen.CategorySummary} - ) - Screen.CategoryCreateOnRegister -> CategoryCreateOnRegisterView( - state = viewState, - onFinishedButton = { screenState.value = Screen.Dashboard} //Should go back to the previous Screen, which could be CategorySummary or EntryCreate. - ) - else -> {} - } -} - -fun categoryIdToCategory(category_id: Int?,categoryList: List): Category { - for (category in categoryList){ - if (category.id == category_id) return category - } - return DEFAULT_CATEGORY //If the category wasn't found (or is set to no category) return default -} - -@OptIn(ExperimentalComposeWebSvgApi::class) -@Composable -fun Bar(category: Category, entryList: List){ - //width and height are for aspect ratio - tries to fill out wherever its in, so its more like - val width = 200 - val height = 80 - val budget = category.budget - var usedBudget = 0f - for (entry in entryList) { - usedBudget+= entry.amount - } - Div{ - if (usedBudget < budget) { - //Money Text - Div(attrs={style { - display(DisplayStyle.Flex) - justifyContent(JustifyContent("space-between")) - }}){ - Div{Text(usedBudget.toString()+"€")} - Div{Text(budget.toString()+"€")} - } - //Bar - Svg(viewBox = "0 0 $width $height"){//For aspect ratio - tries to fill out wherever its in - Rect(x = 0, y = 0, width = width, height = height, { - attr("fill", Color.lightgray.toString()) - }) - Rect(x = 0, y = 0, width = usedBudget/budget*width, height = height, { - attr("fill", Color.darkred.toString()) - }) - } - } - else{ - //SpentBudget Text - Div(attrs={style { - display(DisplayStyle.Flex) - justifyContent(JustifyContent("center")) - }}){ - Div{Text("Budget limit for "+category.name+" reached! "+usedBudget.toString()+"€ of "+budget.toString()+"€ Budget spent")} - } - //Bar - Svg(viewBox = "0 0 $width $height"){//For aspect ratio - tries to fill out wherever its in - Rect(x = 0, y = 0, width = width, height = height, { - attr("fill", Color.red.toString()) - }) - } - } - } -} - diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategoryCreateView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategoryCreateView.kt deleted file mode 100644 index a889b22d..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategoryCreateView.kt +++ /dev/null @@ -1,36 +0,0 @@ -package de.hsfl.budgetBinder.compose.category - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.presentation.UiState -import org.jetbrains.compose.web.dom.Button -import org.jetbrains.compose.web.dom.Div -import org.jetbrains.compose.web.dom.H1 -import org.jetbrains.compose.web.dom.Text - - -@Composable -fun CategoryCreateView( - state: State, - onBackButton: () -> Unit -) { - val viewState by remember { state } - H1{Text("CategoryCreateView")} - Div { - when (viewState) { - is UiState.Success<*> -> { - Text((viewState as UiState.Success<*>).element.toString()) - } - is UiState.Error -> { - Text((viewState as UiState.Error).error) - } - is UiState.Loading -> { - //CircularProgressIndicator() - } - } - Button(attrs = { - onClick { onBackButton() } - }) { - Text("Back to To Category Overview") - } - } -} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategoryEditView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategoryEditView.kt deleted file mode 100644 index 892e8511..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategoryEditView.kt +++ /dev/null @@ -1,36 +0,0 @@ -package de.hsfl.budgetBinder.compose.category - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.presentation.UiState -import org.jetbrains.compose.web.dom.Button -import org.jetbrains.compose.web.dom.Div -import org.jetbrains.compose.web.dom.H1 -import org.jetbrains.compose.web.dom.Text - - -@Composable -fun CategoryEditView( - state: State, - onBackButton: () -> Unit -) { - val viewState by remember { state } - H1{Text("CategoryEditView")} - Div { - when (viewState) { - is UiState.Success<*> -> { - Text((viewState as UiState.Success<*>).element.toString()) - } - is UiState.Error -> { - Text((viewState as UiState.Error).error) - } - is UiState.Loading -> { - //CircularProgressIndicator() - } - } - Button(attrs = { - onClick { onBackButton() } - }) { - Text("Back to Category Overview") - } - } -} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategorySummaryView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategorySummaryView.kt deleted file mode 100644 index a9cefeff..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategorySummaryView.kt +++ /dev/null @@ -1,48 +0,0 @@ -package de.hsfl.budgetBinder.compose.category - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.presentation.UiState -import org.jetbrains.compose.web.dom.Button -import org.jetbrains.compose.web.dom.Div -import org.jetbrains.compose.web.dom.H1 -import org.jetbrains.compose.web.dom.Text - - -@Composable -fun CategorySummaryView( - state: State, - onBackButton: () -> Unit, - onEditButton: () -> Unit, - onCategoryCreateButton: () -> Unit -) { - val viewState by remember { state } - H1{Text("CategorySummaryView")} - Div { - when (viewState) { - is UiState.Success<*> -> { - Text((viewState as UiState.Success<*>).element.toString()) - } - is UiState.Error -> { - Text((viewState as UiState.Error).error) - } - is UiState.Loading -> { - //CircularProgressIndicator() - } - } - Button(attrs = { - onClick { onBackButton() } - }) { - Text("Back to Dashboard") - } - Button(attrs = { - onClick { onEditButton() } - }) { - Text("Edit Category (Needs to be set for each category)") - } - Button(attrs = { - onClick { onCategoryCreateButton() } - }) { - Text("Create Category") - } - } -} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/dashboard/DashboardComponent.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/dashboard/DashboardComponent.kt deleted file mode 100644 index c7237976..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/dashboard/DashboardComponent.kt +++ /dev/null @@ -1,32 +0,0 @@ -package de.hsfl.budgetBinder.compose.dashboard - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.presentation.Screen -import de.hsfl.budgetBinder.presentation.viewmodel.dashboard.DashboardViewModel -import di -import org.kodein.di.instance - -@Composable -fun DashboardComponent(screenState: MutableState) { - //val scope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) - val scope = rememberCoroutineScope() - /*val di = localDI() - val getAllEntriesUseCase: GetAllEntriesUseCase by di.instance() - val getAllCategoriesUseCase: GetAllCategoriesUseCase by di.instance() - val dashboardViewModel = DashboardViewModel(getAllEntriesUseCase,getAllCategoriesUseCase, scope) - val categoriesViewState = dashboardViewModel.categoriesState.collectAsState(scope) - val entriesViewState = dashboardViewModel.entriesState.collectAsState(scope)*/ - - val viewModel: DashboardViewModel by di.instance() - val categoriesViewState = viewModel.categoriesState.collectAsState(scope.coroutineContext) - val entriesViewState = viewModel.entriesState.collectAsState(scope.coroutineContext) - - DashboardView( - categoriesState = categoriesViewState, - entriesState = entriesViewState, - onCategorySummaryButton = { screenState.value = Screen.CategorySummary}, - onSettingsButton = {screenState.value = Screen._Settings}, - onEntryCreateButton = {screenState.value = Screen.EntryCreate}, - onEntryEditButton = {screenState.value = Screen.EntryEdit} - ) -} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/dashboard/DashboardView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/dashboard/DashboardView.kt deleted file mode 100644 index 9d91d87e..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/dashboard/DashboardView.kt +++ /dev/null @@ -1,112 +0,0 @@ -package de.hsfl.budgetBinder.compose.dashboard - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.common.Category -import de.hsfl.budgetBinder.common.Entry -import de.hsfl.budgetBinder.compose.category.Bar -import de.hsfl.budgetBinder.compose.entry.EntryList -import de.hsfl.budgetBinder.presentation.UiState -import org.jetbrains.compose.web.dom.Button -import org.jetbrains.compose.web.dom.Div -import org.jetbrains.compose.web.dom.H1 -import org.jetbrains.compose.web.dom.Text -import kotlin.math.log - - -@Composable -fun DashboardView( - categoriesState: State, - entriesState: State, - onCategorySummaryButton: () -> Unit, - onSettingsButton: () -> Unit, - onEntryCreateButton: () -> Unit, - onEntryEditButton: () -> Unit -) { - val categoriesViewState by remember { categoriesState } - val entriesViewState by remember { entriesState } - var categoryList by remember { mutableStateOf>(emptyList()) } - - var entryList by remember { mutableStateOf>(emptyList()) } - - Div { - H1 { Text("DashboardView") } - Button(attrs = { - onClick { onSettingsButton() } - }) { - Text("Open Settings") - } - Button(attrs = { - onClick { onCategorySummaryButton() } - }) { - Text("Open Category List (Summary of every Category)") - } - Button(attrs = { - onClick { onEntryCreateButton() } - }) { - Text("Create Entry") - } - Button(attrs = { - onClick { onEntryEditButton() } - }) { - Text("Edit Entry (Needs to be there for every Entry shown)") - } - } - Div { - UpdateDashboardData(categoryList, entryList) - } - //Process new Category Data - when (categoriesViewState) { - is UiState.Success<*> -> { - //Updates Data - // https://stackoverflow.com/questions/36569421/kotlin-how-to-work-with-list-casts-unchecked-cast-kotlin-collections-listkot - when (val element = (categoriesViewState as UiState.Success<*>).element) { - is List<*> -> { - element.filterIsInstance() - .let { - if (it.size == element.size) { - categoryList = it - } - } - } - } - } - is UiState.Error -> { - Text((categoriesViewState as UiState.Error).error) - } - is UiState.Loading -> { - //CircularProgressIndicator() - } - } - //Process new Entry Data - when (entriesViewState) { - is UiState.Success<*> -> { - //Updates Data - when (val element = (entriesViewState as UiState.Success<*>).element) { - is List<*> -> { - element.filterIsInstance() - .let { - if (it.size == element.size) { - entryList = it - } - } - } - } - - } - is UiState.Error -> { - Text((entriesViewState as UiState.Error).error) - } - is UiState.Loading -> { - //CircularProgressIndicator() - } - } -} - -@Composable -fun UpdateDashboardData(categoryList: List, entryList: List) { - console.log("Category $categoryList and Entry $entryList") - if (categoryList.isNotEmpty() && entryList.isNotEmpty()) { - Bar(categoryList[0], entryList) //Bar for first Category, needs to be changed later - EntryList(entryList, categoryList) - } -} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/entry/EntryComponent.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/entry/EntryComponent.kt deleted file mode 100644 index 2e78928f..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/entry/EntryComponent.kt +++ /dev/null @@ -1,66 +0,0 @@ -package de.hsfl.budgetBinder.compose.entry - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.common.Category -import de.hsfl.budgetBinder.common.Entry -import de.hsfl.budgetBinder.compose.category.categoryIdToCategory -import de.hsfl.budgetBinder.domain.usecase.* -import de.hsfl.budgetBinder.presentation.CategoryImageToIcon -import de.hsfl.budgetBinder.presentation.Screen -import de.hsfl.budgetBinder.presentation.viewmodel.EntryViewModel -import di -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import org.jetbrains.compose.web.dom.Div -import org.jetbrains.compose.web.dom.Text -import org.kodein.di.compose.localDI -import org.kodein.di.instance - -@Composable -fun EntryComponent(screenState: MutableState) { - //val scope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) - val scope = rememberCoroutineScope() - /*val di = localDI() - val getAllEntriesUseCase: GetAllEntriesUseCase by di.instance() - val getEntryByIdUseCase: GetEntryByIdUseCase by di.instance() - val changeEntryByIdUseCase: ChangeEntryByIdUseCase by di.instance() - val deleteEntryByIdUseCase: DeleteEntryByIdUseCase by di.instance() - val createNewEntryUseCase: CreateNewEntryUseCase by di.instance() - val userViewModel = EntryViewModel(getAllEntriesUseCase, getEntryByIdUseCase, createNewEntryUseCase,changeEntryByIdUseCase,deleteEntryByIdUseCase, scope) - val viewState = userViewModel.state.collectAsState(scope)*/ - - val viewModel: EntryViewModel by di.instance() - val viewState = viewModel.state.collectAsState(scope.coroutineContext) - - when (screenState.value) { - Screen.EntryCreate -> EntryCreateView( - state = viewState, - onBackButton = { screenState.value = Screen.Dashboard }, - onCategoryCreateButton = { screenState.value = Screen.CategoryCreate } - ) - Screen.EntryEdit -> EntryEditView( - state = viewState, - onBackButton = { screenState.value = Screen.Dashboard} - ) - else -> {} - } -} - -//Should be put in own File -@Composable -fun EntryListElement(entry: Entry, categoryList : List){ - Div { - CategoryImageToIcon(categoryIdToCategory(entry.category_id,categoryList).image) - Text(entry.name) - Text(entry.amount.toString()+"€") - - } -} - -@Composable -fun EntryList(list: List, categoryList : List){ - for (entry in list){ - EntryListElement(entry,categoryList) - } -} \ No newline at end of file diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/entry/EntryCreateView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/entry/EntryCreateView.kt deleted file mode 100644 index 40b47c7c..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/entry/EntryCreateView.kt +++ /dev/null @@ -1,42 +0,0 @@ -package de.hsfl.budgetBinder.compose.entry - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.presentation.UiState -import org.jetbrains.compose.web.dom.Button -import org.jetbrains.compose.web.dom.Div -import org.jetbrains.compose.web.dom.H1 -import org.jetbrains.compose.web.dom.Text - - -@Composable -fun EntryCreateView( - state: State, - onBackButton: () -> Unit, - onCategoryCreateButton: () -> Unit -) { - val viewState by remember { state } - H1{Text("EntryCreate")} - Div { - when (viewState) { - is UiState.Success<*> -> { - Text((viewState as UiState.Success<*>).element.toString()) - } - is UiState.Error -> { - Text((viewState as UiState.Error).error) - } - is UiState.Loading -> { - //CircularProgressIndicator() - } - } - Button(attrs = { - onClick { onBackButton() } - }) { - Text("Back to Dashboard") - } - Button(attrs = { - onClick { onCategoryCreateButton() } - }) { - Text("Create new Category (Needs to be put as the last option when selecting a category for an entry)") - } - } -} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/entry/EntryEditView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/entry/EntryEditView.kt deleted file mode 100644 index 0aac23a6..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/entry/EntryEditView.kt +++ /dev/null @@ -1,36 +0,0 @@ -package de.hsfl.budgetBinder.compose.entry - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.presentation.UiState -import org.jetbrains.compose.web.dom.Button -import org.jetbrains.compose.web.dom.Div -import org.jetbrains.compose.web.dom.H1 -import org.jetbrains.compose.web.dom.Text - - -@Composable -fun EntryEditView( - state: State, - onBackButton: () -> Unit -) { - val viewState by remember { state } - H1{Text("EntryEditView")} - Div { - when (viewState) { - is UiState.Success<*> -> { - Text((viewState as UiState.Success<*>).element.toString()) - } - is UiState.Error -> { - Text((viewState as UiState.Error).error) - } - is UiState.Loading -> { - //CircularProgressIndicator() - } - } - Button(attrs = { - onClick { onBackButton() } - }) { - Text("Back to Dashboard") - } - } -} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/login/LoginComponent.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/login/LoginComponent.kt deleted file mode 100644 index ecd10e54..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/login/LoginComponent.kt +++ /dev/null @@ -1,29 +0,0 @@ -package de.hsfl.budgetBinder.compose.login -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.presentation.Screen -import de.hsfl.budgetBinder.presentation.viewmodel.auth.login.LoginViewModel -import di -import org.kodein.di.compose.withDI -import org.kodein.di.instance - - -@Composable -fun LoginComponent(screenState: MutableState) = withDI(di) { - /*val scope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) - val loginUseCase: LoginUseCase by di.instance() - val getMyUserUseCase : GetMyUserUseCase by di.instance() - val viewModel = LoginViewModel(scope,loginUseCase, getMyUserUseCase) - val viewState = viewModel.state.collectAsState(scope)*/ - val scope = rememberCoroutineScope() - val viewModel: LoginViewModel by di.instance() - val viewState = viewModel.state.collectAsState(scope) - - LoginView( - state = viewState, - onLoginButtonPressed = { email, password -> - viewModel._login(email, password) - }, - onLoginSuccess = { screenState.value = Screen.Dashboard }, - onChangeToRegister = { screenState.value = Screen.Register } - ) -} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/login/LoginView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/login/LoginView.kt deleted file mode 100644 index 82e7ed1c..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/login/LoginView.kt +++ /dev/null @@ -1,187 +0,0 @@ -package de.hsfl.budgetBinder.compose.login - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.compose.MainFlexContainer -import de.hsfl.budgetBinder.compose.theme.AppStylesheet -import de.hsfl.budgetBinder.presentation.UiState -import org.jetbrains.compose.web.css.* -import org.jetbrains.compose.web.dom.* - -@Composable -fun LoginView( - state: State, - onLoginButtonPressed: (email: String, password: String) -> Unit, - onLoginSuccess: () -> Unit, - onChangeToRegister: () -> Unit -) { - var emailTextFieldState by remember { mutableStateOf("") } - var passwordTextFieldState by remember { mutableStateOf("") } - val viewState by remember { state } - - Header( - attrs = { - classes("mdc-top-app-bar") - } - ) { - Div( - attrs = { - classes("mdc-top-app-bar__row") - } - ) { - Section( - attrs = { - classes("mdc-top-app-bar__section", "mdc-top-app-bar__section--align-start") - } - ) { - Img( - src = "images/Logo.png", alt = "Logo", attrs = { - classes("mdc-icon-button", AppStylesheet.image) - } - ) - Span( - attrs = { - classes("mdc-top-app-bar__title") - } - ) { - Text("Budget-Binder") - } - } - Section( - attrs = { - classes("mdc-top-app-bar__section", "mdc-top-app-bar__section--align-end") - } - ) { - Button( - attrs = { - classes("mdc-button", "mdc-button--raised", "mdc-top-app-bar__navigation-icon") - onClick { onChangeToRegister() } - } - ) { - Span( - attrs = { - classes("mdc-button__label") - } - ) { - Text("Register Instead") - } - } - } - } - } - - MainFlexContainer{ - // -- Login Form -- - Div( - attrs = { - classes("mdc-card", AppStylesheet.card) - } - ) { - H1 { Text(" Login") } - Form( - attrs = { - this.addEventListener("submit") { - console.log("${emailTextFieldState}, ${passwordTextFieldState}") - onLoginButtonPressed(emailTextFieldState, passwordTextFieldState) - it.preventDefault() - } - } - ) { - Div( - attrs = { - classes(AppStylesheet.margin) - } - ) { - Label( - attrs = { - classes("mdc-text-field", "mdc-text-field--filled") - style { width(100.percent) } - } - ) { - Span( - attrs = { - classes("mdc-text-field__ripple") - } - ) { } - Span( - attrs = { - classes("mdc-floating-label", "mdc-floating-label--float-above") - } - ) { Text("Email") } - EmailInput(value = emailTextFieldState, - attrs = { - classes("mdc-text-field__input") - onInput { - emailTextFieldState = it.value - - } - }) - Span( - attrs = { - classes("mdc-line-ripple") - } - ) { } - } - } - Div( - attrs = { - classes(AppStylesheet.margin) - } - ) { - Label( - attrs = { - classes("mdc-text-field", "mdc-text-field--filled") - style { width(100.percent) } - } - ) { - Span( - attrs = { - classes("mdc-text-field__ripple") - } - ) { } - Span( - attrs = { - classes("mdc-floating-label", "mdc-floating-label--float-above") - } - ) { Text("Password") } - PasswordInput(value = passwordTextFieldState, - attrs = { - classes("mdc-text-field__input") - onInput { - passwordTextFieldState = it.value - } - }) - Span( - attrs = { - classes("mdc-line-ripple") - } - ) { } - } - } - Div( - attrs = { - classes(AppStylesheet.margin) - } - ) { - SubmitInput( - attrs = { - classes("mdc-button", "mdc-button--raised") - value("Submit") - }) - } - // -- Login Request Management -- - when (viewState) { - is UiState.Success<*> -> { - onLoginSuccess() - } - is UiState.Error -> { - Text((viewState as UiState.Error).error) - } - is UiState.Loading -> { - //CircularProgressIndicator() - } - else -> {} - } - } - } - } -} \ No newline at end of file diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/register/RegisterComponent.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/register/RegisterComponent.kt deleted file mode 100644 index 0dd246e8..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/register/RegisterComponent.kt +++ /dev/null @@ -1,32 +0,0 @@ -package de.hsfl.budgetBinder.compose.register - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.common.User - -import de.hsfl.budgetBinder.presentation.Screen -import de.hsfl.budgetBinder.presentation.viewmodel.auth.register.RegisterViewModel -import di -import org.kodein.di.instance - - -@Composable -fun RegisterComponent(screenState: MutableState) { - /*al scope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) - val registerUseCase: RegisterUseCase by di.instance() - val loginUseCase: LoginUseCase by di.instance() - val getMyUserUseCase : GetMyUserUseCase by di.instance() - val viewModel = RegisterViewModel(registerUseCase,loginUseCase,getMyUserUseCase, scope) - val viewState = viewModel.state.collectAsState(scope)*/ - val scope = rememberCoroutineScope() - val viewModel: RegisterViewModel by di.instance() - val viewState = viewModel.state.collectAsState(scope.coroutineContext) - - - RegisterView( - state = viewState, - onRegisterButtonPressed = { firstName, lastName, email, password -> - viewModel._register(User.In(firstName,lastName,email,password)) - }, - onChangeToLogin = { screenState.value = Screen.Login } - ) -} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/register/RegisterView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/register/RegisterView.kt deleted file mode 100644 index 9cb0697a..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/register/RegisterView.kt +++ /dev/null @@ -1,271 +0,0 @@ -package de.hsfl.budgetBinder.compose.register - - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.compose.MainFlexContainer -import de.hsfl.budgetBinder.compose.theme.AppStylesheet -import de.hsfl.budgetBinder.presentation.UiState -import org.jetbrains.compose.web.attributes.InputType -import org.jetbrains.compose.web.css.flex -import org.jetbrains.compose.web.css.percent -import org.jetbrains.compose.web.css.width -import org.jetbrains.compose.web.dom.* - -@Composable -fun RegisterView( - state: State, - onRegisterButtonPressed: (firstName: String, lastName: String, email: String, password: String) -> Unit, - onChangeToLogin: () -> Unit -) { - var firstNameTextFieldState by remember { mutableStateOf("") } - var lastNameTextFieldState by remember { mutableStateOf("") } - var emailTextFieldState by remember { mutableStateOf("") } - var passwordTextFieldState by remember { mutableStateOf("") } - val viewState by remember { state } - - Header( - attrs = { - classes("mdc-top-app-bar") - } - ) { - Div( - attrs = { - classes("mdc-top-app-bar__row") - } - ) { - Section( - attrs = { - classes("mdc-top-app-bar__section", "mdc-top-app-bar__section--align-start") - } - ) { - Img( - src = "images/Logo.png", alt = "Logo", attrs = { - classes("mdc-icon-button", AppStylesheet.image) - } - ) - Span( - attrs = { - classes("mdc-top-app-bar__title") - } - ) { - Text("Budget-Binder") - } - } - Section( - attrs = { - classes("mdc-top-app-bar__section", "mdc-top-app-bar__section--align-end") - } - ) { - Button( - attrs = { - classes("mdc-button", "mdc-button--raised", "mdc-top-app-bar__navigation-icon") - onClick { onChangeToLogin() } - } - ) { - Span( - attrs = { - classes("mdc-button__label") - } - ) { - Text("Login Instead") - } - } - } - } - } - - MainFlexContainer{ - // -- Register Form -- - Div( - attrs = { - classes("mdc-card", AppStylesheet.card) - } - ) { - - H1 { Text(" Register") } - Form(attrs = { //Probably possible with just a button OnClick instead of Form&Submit - this.addEventListener("submit") { - console.log("$firstNameTextFieldState, $lastNameTextFieldState, $emailTextFieldState, $passwordTextFieldState") - onRegisterButtonPressed( - firstNameTextFieldState, - lastNameTextFieldState, - emailTextFieldState, - passwordTextFieldState - ) - it.preventDefault() - } - } - ) { - Div( - attrs = { - classes(AppStylesheet.margin) - } - ) { - Label( - attrs = { - classes("mdc-text-field", "mdc-text-field--filled") - style { width(100.percent) } - } - ) { - Span( - attrs = { - classes("mdc-text-field__ripple") - } - ) { } - Span( - attrs = { - classes("mdc-floating-label", "mdc-floating-label--float-above") - } - ) { Text("Firstname") } - Input( - type = InputType.Text - ) { - classes("mdc-text-field__input") - value(firstNameTextFieldState) - onInput { - firstNameTextFieldState = it.value - } - } - Span( - attrs = { - classes("mdc-line-ripple") - } - ) { } - } - } - Div( - attrs = { - classes(AppStylesheet.margin) - } - ) { - Label( - attrs = { - classes("mdc-text-field", "mdc-text-field--filled") - style { width(100.percent) } - } - ) { - Span( - attrs = { - classes("mdc-text-field__ripple") - } - ) { } - Span( - attrs = { - classes("mdc-floating-label", "mdc-floating-label--float-above") - } - ) { Text("Lastname") } - Input( - type = InputType.Text - ) { - classes("mdc-text-field__input") - value(lastNameTextFieldState) - onInput { - lastNameTextFieldState = it.value - } - } - Span( - attrs = { - classes("mdc-line-ripple") - } - ) { } - } - } - Div( - attrs = { - classes(AppStylesheet.margin) - } - ) { - Label( - attrs = { - classes("mdc-text-field", "mdc-text-field--filled") - style { width(100.percent) } - } - ) { - Span( - attrs = { - classes("mdc-text-field__ripple") - } - ) { } - Span( - attrs = { - classes("mdc-floating-label", "mdc-floating-label--float-above") - } - ) { Text("Email") } - EmailInput(value = emailTextFieldState, - attrs = { - classes("mdc-text-field__input") - onInput { - emailTextFieldState = it.value - - } - }) - Span( - attrs = { - classes("mdc-line-ripple") - } - ) { } - } - } - Div( - attrs = { - classes(AppStylesheet.margin) - } - ) { - Label( - attrs = { - classes("mdc-text-field", "mdc-text-field--filled") - style { width(100.percent) } - } - ) { - Span( - attrs = { - classes("mdc-text-field__ripple") - } - ) { } - Span( - attrs = { - classes("mdc-floating-label", "mdc-floating-label--float-above") - } - ) { Text("Password") } - PasswordInput(value = passwordTextFieldState, - attrs = { - classes("mdc-text-field__input") - onInput { - passwordTextFieldState = it.value - } - }) - Span( - attrs = { - classes("mdc-line-ripple") - } - ) { } - } - } - Div( - attrs = { - classes(AppStylesheet.margin) - } - ) { - SubmitInput( - attrs = { - classes("mdc-button", "mdc-button--raised") - value("Submit") - }) - } - // -- Register Request Management -- - when (viewState) { - is UiState.Success<*> -> { - onChangeToLogin() - } - is UiState.Error -> { - Text((viewState as UiState.Error).error) - } - is UiState.Loading -> { - //CircularProgressIndicator() - } - else -> {} - } - } - } - } -} \ No newline at end of file diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/settings/SettingsChangeUserDataView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/settings/SettingsChangeUserDataView.kt deleted file mode 100644 index 30b8488d..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/settings/SettingsChangeUserDataView.kt +++ /dev/null @@ -1,220 +0,0 @@ -package de.hsfl.budgetBinder.compose.settings - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.compose.MainFlexContainer -import de.hsfl.budgetBinder.compose.theme.AppStylesheet -import de.hsfl.budgetBinder.compose.topBarMain -import de.hsfl.budgetBinder.presentation.UiState -import org.jetbrains.compose.web.attributes.InputType -import org.jetbrains.compose.web.css.percent -import org.jetbrains.compose.web.css.width -import org.jetbrains.compose.web.dom.* - - -@Composable -fun SettingsChangeUserDataView( - state: State, - onChangeToDashboard: () -> Unit, - onChangeToSettings: () -> Unit, - onChangeToCategory: () -> Unit, - onChangeDataButtonPressed: (firstName: String, lastName: String, password: String) -> Unit -) { - var firstNameTextFieldState by remember { mutableStateOf("") } - var lastNameTextFieldState by remember { mutableStateOf("") } - var passwordTextFieldState by remember { mutableStateOf("") } - val viewState by remember { state } - - topBarMain( - logoButton = { - Img( - src = "images/Logo.png", alt = "Logo", attrs = { - classes("mdc-icon-button", AppStylesheet.image) - onClick { onChangeToDashboard() } - } - ) - }, navButtons = { - Button( - attrs = { - classes("mdc-button", "mdc-button--raised", "mdc-top-app-bar__navigation-icon") - onClick { onChangeToCategory() } - } - ) { - Span( - attrs = { - classes("mdc-button__label") - } - ) { - Text("Categories") - } - } - Button( - attrs = { - classes("mdc-button", "mdc-button--raised", "mdc-top-app-bar__navigation-icon") - onClick { onChangeToSettings() } - } - ) { - Span( - attrs = { - classes("mdc-button__label") - } - ) { - Text("Settings") - } - } - }) - - MainFlexContainer { - Div( - attrs = { - classes("mdc-card", AppStylesheet.card) - } - ) { - - H1 { Text("Change User Data") } - Form(attrs = { - this.addEventListener("submit") { - console.log("$firstNameTextFieldState, $lastNameTextFieldState, $passwordTextFieldState") - onChangeDataButtonPressed( - firstNameTextFieldState, - lastNameTextFieldState, - passwordTextFieldState - ) - it.preventDefault() - } - } - ) { - Div( - attrs = { - classes(AppStylesheet.margin) - } - ) { - Label( - attrs = { - classes("mdc-text-field", "mdc-text-field--filled") - style { width(100.percent) } - } - ) { - Span( - attrs = { - classes("mdc-text-field__ripple") - } - ) { } - Span( - attrs = { - classes("mdc-floating-label", "mdc-floating-label--float-above") - } - ) { Text("Firstname") } - Input( - type = InputType.Text - ) { - classes("mdc-text-field__input") - value(firstNameTextFieldState) - onInput { - firstNameTextFieldState = it.value - } - } - Span( - attrs = { - classes("mdc-line-ripple") - } - ) { } - } - } - Div( - attrs = { - classes(AppStylesheet.margin) - } - ) { - Label( - attrs = { - classes("mdc-text-field", "mdc-text-field--filled") - style { width(100.percent) } - } - ) { - Span( - attrs = { - classes("mdc-text-field__ripple") - } - ) { } - Span( - attrs = { - classes("mdc-floating-label", "mdc-floating-label--float-above") - } - ) { Text("Lastname") } - Input( - type = InputType.Text - ) { - classes("mdc-text-field__input") - value(lastNameTextFieldState) - onInput { - lastNameTextFieldState = it.value - } - } - Span( - attrs = { - classes("mdc-line-ripple") - } - ) { } - } - } - Div( - attrs = { - classes(AppStylesheet.margin) - } - ) { - Label( - attrs = { - classes("mdc-text-field", "mdc-text-field--filled") - style { width(100.percent) } - } - ) { - Span( - attrs = { - classes("mdc-text-field__ripple") - } - ) { } - Span( - attrs = { - classes("mdc-floating-label", "mdc-floating-label--float-above") - } - ) { Text("Password") } - PasswordInput(value = passwordTextFieldState, - attrs = { - classes("mdc-text-field__input") - onInput { - passwordTextFieldState = it.value - } - }) - Span( - attrs = { - classes("mdc-line-ripple") - } - ) { } - } - } - Div( - attrs = { - classes(AppStylesheet.margin) - } - ) { - SubmitInput( - attrs = { - classes("mdc-button", "mdc-button--raised") - value("Submit") - }) - } - when (viewState) { - is UiState.Success<*> -> { - Text((viewState as UiState.Success<*>).element.toString()) - } - is UiState.Error -> { - Text((viewState as UiState.Error).error) - } - is UiState.Loading -> { - //CircularProgressIndicator() - } - } - } - } - } -} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/settings/SettingsComponent.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/settings/SettingsComponent.kt deleted file mode 100644 index a52e7127..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/settings/SettingsComponent.kt +++ /dev/null @@ -1,44 +0,0 @@ -package de.hsfl.budgetBinder.compose.settings - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.common.User -import de.hsfl.budgetBinder.presentation.Screen -import de.hsfl.budgetBinder.presentation.viewmodel.settings.SettingsViewModel -import di -import org.kodein.di.instance - -@Composable -fun SettingsComponent(screenState: MutableState) { - /*val scope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) - val di = localDI() - val changeMyUserUseCase: ChangeMyUserUseCase by di.instance() - val deleteMyUserUseCase: DeleteMyUserUseCase by di.instance() - val settingsViewModel = SettingsViewModel(changeMyUserUseCase, deleteMyUserUseCase, scope) - val viewState = settingsViewModel.state.collectAsState(scope)*/ - val scope = rememberCoroutineScope() - val settingsViewModel: SettingsViewModel by di.instance() - val viewState = settingsViewModel.state.collectAsState(scope) - - - when (screenState.value) { - Screen._Settings -> SettingsView( - state = viewState, - onChangeToDashboard = { screenState.value = Screen.Dashboard }, - onChangeToSettings = { screenState.value = Screen._Settings }, - onChangeToCategory = { screenState.value = Screen.CategorySummary }, - onDeleteButtonPressed = { settingsViewModel.deleteMyUser(); screenState.value = Screen.Login }, - onChangeButtonPressed = { screenState.value = Screen.SettingsChangeUserData } - ) - Screen.SettingsChangeUserData -> SettingsChangeUserDataView( - state = viewState, - onChangeDataButtonPressed = { firstName, lastName, password -> - settingsViewModel.changeMyUser(User.Patch(firstName, lastName, password)); screenState.value = - Screen._Settings - }, - onChangeToDashboard = { screenState.value = Screen.Dashboard }, - onChangeToSettings = { screenState.value = Screen._Settings }, - onChangeToCategory = { screenState.value = Screen.CategorySummary }, - ) - else -> {} - } -} \ No newline at end of file diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/settings/SettingsView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/settings/SettingsView.kt deleted file mode 100644 index 9583303b..00000000 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/settings/SettingsView.kt +++ /dev/null @@ -1,117 +0,0 @@ -package de.hsfl.budgetBinder.compose.settings - -import androidx.compose.runtime.* -import de.hsfl.budgetBinder.presentation.UiState -import de.hsfl.budgetBinder.compose.MainFlexContainer -import de.hsfl.budgetBinder.compose.theme.AppStylesheet -import de.hsfl.budgetBinder.compose.topBarMain -import org.jetbrains.compose.web.css.flex -import org.jetbrains.compose.web.css.marginLeft -import org.jetbrains.compose.web.css.percent -import org.jetbrains.compose.web.dom.* - - -@Composable -fun SettingsView( - state: State, - onChangeToDashboard: () -> Unit, - onChangeToSettings: () -> Unit, - onChangeToCategory: () -> Unit, - onDeleteButtonPressed: () -> Unit, - onChangeButtonPressed: () -> Unit -) { - val viewState by remember { state } - - topBarMain( - logoButton = { - Img( - src = "images/Logo.png", alt = "Logo", attrs = { - classes("mdc-icon-button", AppStylesheet.image) - onClick { onChangeToDashboard() } - } - ) - }, navButtons = { - Button( - attrs = { - classes("mdc-button", "mdc-button--raised", "mdc-top-app-bar__navigation-icon") - onClick { onChangeToCategory() } - } - ) { - Span( - attrs = { - classes("mdc-button__label") - } - ) { - Text("Categories") - } - } - Button( - attrs = { - classes("mdc-button", "mdc-button--raised", "mdc-top-app-bar__navigation-icon") - onClick { onChangeToSettings() } - } - ) { - Span( - attrs = { - classes("mdc-button__label") - } - ) { - Text("Settings") - } - } - }) - - MainFlexContainer { - Div(attrs = { - classes("mdc-card", AppStylesheet.card) - } - ) { - H1( - attrs = { - style { marginLeft(2.percent) } - } - ) { Text("Settings") } - when (viewState) { - is UiState.Success<*> -> { - Text((viewState as UiState.Success<*>).element.toString()) - } - is UiState.Error -> { - Text((viewState as UiState.Error).error) - } - is UiState.Loading -> { - //CircularProgressIndicator() - } - } - Div( - attrs = { - classes(AppStylesheet.margin, AppStylesheet.flexContainer) - } - ) { - Button( - attrs = { - classes("mdc-button", "mdc-button--raised") - onClick { onChangeButtonPressed() } - style { flex(100.percent) } - } - ) { - Text("Change Userdata") - } - } - Div( - attrs = { - classes(AppStylesheet.margin, AppStylesheet.flexContainer) - } - ) { - Button( - attrs = { - classes("mdc-button", "mdc-button--raised") - onClick { onDeleteButtonPressed() } - style { flex(100.percent) } - } - ) { - Text("Delete User") - } - } - } - } -} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/theme/AppStylesheet.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/theme/AppStylesheet.kt index b498f7f6..2edd1067 100644 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/theme/AppStylesheet.kt +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/theme/AppStylesheet.kt @@ -1,6 +1,7 @@ package de.hsfl.budgetBinder.compose.theme import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.css.keywords.auto import org.jetbrains.compose.web.css.selectors.CSSSelector /*All Information found about Stylesheets: @@ -36,7 +37,9 @@ object AppStylesheet : StyleSheet() { //Container for flex elements, used in MainFlexContainer val flexContainer by style { display(DisplayStyle.Flex) + justifyContent(JustifyContent.Center) } + //Container for empty sides, used in MainFlexContainer val pufferFlexContainer by style { media(mediaMaxWidth(1000.px)) { @@ -46,21 +49,74 @@ object AppStylesheet : StyleSheet() { } flex("25%") } + //Container for main content, used in MainFlexContainer val contentFlexContainer by style { - justifyContent(JustifyContent.Center) flex("50%") + position(Position.Relative) + } + + //Container for main content in BudgetBar + val budgetBarContainer by style { + flex("90%") + } + + //Container for arrow in BudgetBar + val arrowFlexContainer by style { + flex("0.1 0.1 5%") + height(auto) + } + + + val categoryImageList by style { + justifyContent(JustifyContent.Center) + } + + val text by style { + textAlign("center") + padding(10.px) } //EntryList - val entryListElement by style{ + val entryListElement by style { + flexDirection(FlexDirection("row")) + alignItems(AlignItems.Center) + margin(10.px) + marginTop(0.px) + } + val entryListElementText by style { + flex("2 2 90%") + } + val moneyText by style { + textAlign("center") + padding(10.px) + whiteSpace("nowrap") + } + val newEntryButton by style { + position(Position.Fixed) + bottom(16.px) + marginRight(20.px) + } + val categoryListElement by style { + flexDirection(FlexDirection("row")) + alignItems(AlignItems.Center) + margin(10.px) + marginTop(0.px) + } + + val categoryListElementText by style { + flex("2 2 90%") + } + val imageFlexContainer by style { + flex("0.1 0.1 5%") + height(auto) } + val card by style { margin(10.px) marginTop(25.px) - } val image by style { @@ -70,4 +126,37 @@ object AppStylesheet : StyleSheet() { val margin by style { margin(10.px) } -} \ No newline at end of file + + val marginRight by style { + marginRight(1.percent) + } + + val loadingImage by style { + property("z-index", 1) + width(20.percent) + position(Position.Fixed) + top(40.percent) + left(40.percent) + } + + val h1 by style { + margin(2.percent) + } + + val width by style { + width(100.percent) + } + + val flex50 by style { + flex(50.percent) + } + + val flex100 by style { + flex(100.percent) + } + + val buttonOverview by style { + flex(33.percent) + margin(1.5.percent) + } +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/presentation/CategoryIcon.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/presentation/CategoryIcon.kt index bedf3bb9..cffd4de2 100644 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/presentation/CategoryIcon.kt +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/presentation/CategoryIcon.kt @@ -2,14 +2,22 @@ package de.hsfl.budgetBinder.presentation import androidx.compose.runtime.Composable import de.hsfl.budgetBinder.common.Category +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Span import org.jetbrains.compose.web.dom.Text @Composable actual fun CategoryImageToIcon(icon: Category.Image) { - Span( + Div(attrs = {classes(AppStylesheet.imageFlexContainer)}){ + Span( attrs = { classes("material-icons") + style { + paddingTop(8.px) + paddingBottom(8.px) + } } ) { Text( @@ -21,7 +29,7 @@ actual fun CategoryImageToIcon(icon: Category.Image) { Category.Image.WRONG -> "dangerous" Category.Image.HOME -> "home" Category.Image.FOOD -> "bakery_dining" - Category.Image.FASTFOOD -> "bakery_dining" + Category.Image.FASTFOOD -> "fastfood" Category.Image.RESTAURANT -> "restaurant" Category.Image.FAMILY -> "people" Category.Image.MONEY -> "payments" @@ -35,10 +43,10 @@ actual fun CategoryImageToIcon(icon: Category.Image) { Category.Image.FLOWER -> "local_florist" Category.Image.PET -> "pets" Category.Image.BILLS -> "receipt" - Category.Image.KEYBOARD -> "redeem" + Category.Image.KEYBOARD -> "keyboard" Category.Image.PRINTER -> "print" Category.Image.WATER -> "water_drop" - Category.Image.FIRE -> "fire" + Category.Image.FIRE -> "local_fire_department" Category.Image.STAR -> "grade" Category.Image.SAVINGS -> "savings" Category.Image.CAR -> "minor_crash" @@ -60,5 +68,5 @@ actual fun CategoryImageToIcon(icon: Category.Image) { Category.Image.PEST -> "pest_control" } ) - } -} \ No newline at end of file + }} +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryComponent.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryComponent.kt new file mode 100644 index 00000000..5e9caf1a --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryComponent.kt @@ -0,0 +1,29 @@ +package de.hsfl.budgetBinder.screens.category + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.compose.MainFlexContainer +import de.hsfl.budgetBinder.compose.NavBar +import de.hsfl.budgetBinder.presentation.Screen +import de.hsfl.budgetBinder.presentation.flow.RouterFlow +import di +import org.kodein.di.instance + +@Composable +fun CategoryComponent() { + + val routerFlow: RouterFlow by di.instance() + + //Webpage content + NavBar {} + MainFlexContainer { + when (routerFlow.state.value) { + is Screen.Category.Summary -> CategorySummaryView() + is Screen.Category.Detail -> CategoryDetailView() + is Screen.Category.Edit -> CategoryEditView() + is Screen.Category.Create -> CategoryCreateView() + else -> {} + } + } +} + + diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategoryCreateOnRegisterView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryCreateOnRegisterView.kt similarity index 83% rename from budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategoryCreateOnRegisterView.kt rename to budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryCreateOnRegisterView.kt index 0101d41b..b4ab1069 100644 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/compose/category/CategoryCreateOnRegisterView.kt +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryCreateOnRegisterView.kt @@ -1,6 +1,7 @@ -package de.hsfl.budgetBinder.compose.category +package de.hsfl.budgetBinder.screens.category import androidx.compose.runtime.* +import de.hsfl.budgetBinder.compose.theme.AppStylesheet import de.hsfl.budgetBinder.presentation.UiState import org.jetbrains.compose.web.dom.Button import org.jetbrains.compose.web.dom.Div @@ -14,7 +15,7 @@ fun CategoryCreateOnRegisterView( onFinishedButton: () -> Unit ) { val viewState by remember { state } - H1{Text("CategoryCreateOnRegisterView")} + H1(attrs = { classes(AppStylesheet.h1) }) {Text("CategoryCreateOnRegisterView")} Div { when (viewState) { is UiState.Success<*> -> { diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryCreateView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryCreateView.kt new file mode 100644 index 00000000..7f0aeb7b --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryCreateView.kt @@ -0,0 +1,208 @@ +package de.hsfl.budgetBinder.screens.category + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.compose.CategoryImagesToImageList +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import de.hsfl.budgetBinder.presentation.event.LifecycleEvent +import de.hsfl.budgetBinder.presentation.viewmodel.category.create.CategoryCreateEvent +import de.hsfl.budgetBinder.presentation.viewmodel.category.create.CategoryCreateViewModel +import di +import org.jetbrains.compose.web.attributes.* +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.* +import org.kodein.di.instance + + +@Composable +fun CategoryCreateView() { + val viewModel: CategoryCreateViewModel by di.instance() + val categoryNameState by viewModel.categoryNameState.collectAsState() + val categoryColorState by viewModel.categoryColorState.collectAsState() + val categoryImageState by viewModel.categoryImageState.collectAsState() + val categoryBudgetState by viewModel.categoryBudgetState.collectAsState() + + //Life Cycle + LaunchedEffect(Unit) { + viewModel.onEvent(CategoryCreateEvent.LifeCycle(LifecycleEvent.OnLaunch)) + } + DisposableEffect(Unit) { + onDispose { + viewModel.onEvent(CategoryCreateEvent.LifeCycle(LifecycleEvent.OnDispose)) + } + } + + //Webpage Content + H1(attrs = { classes(AppStylesheet.h1) }) { Text("Create a new Category") } + Form(attrs = { + this.addEventListener("submit") { + viewModel.onEvent(CategoryCreateEvent.OnSave) + it.preventDefault() + } + } + ) { + //Category Name Input + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled") + style { width(100.percent) } + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Category Name") } + Input( + type = InputType.Text + ) { + classes("mdc-text-field__input") + value(categoryNameState) + required(true) + onInput { + viewModel.onEvent(CategoryCreateEvent.EnteredCategoryName(it.value)) + } + } + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + //Category Color Input + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--outlined") + style { width(100.percent) } + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + style { marginBottom(1.percent) } + } + ) { Text("Color") } + Input( + type = InputType.Color + ) { + classes("mdc-text-field__input") + value("#$categoryColorState") + onInput { + viewModel.onEvent(CategoryCreateEvent.EnteredCategoryColor(it.value.drop(1))) + } + } + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + //Category Image Input + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + style { width(100.percent) } + } + ) { + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + style { marginBottom(1.percent); marginLeft(2.percent) } + } + ) { Text("Image") } + CategoryImagesToImageList( + categoryImageState, + onClick = { viewModel.onEvent(CategoryCreateEvent.EnteredCategoryImage(it)) }) + } + } + //Category Budget Input + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled") + style { width(100.percent) } + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Budget") } + Input( + type = InputType.Number + ) { + attr("step", "0.01") + classes("mdc-text-field__input") + value(categoryBudgetState) + required(true) + min("1") + onInput { + viewModel.onEvent(CategoryCreateEvent.EnteredCategoryBudget(it.value as Float)) + } + } + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + //Submit button + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Button( + attrs = { + classes("mdc-button", "mdc-button--raised", AppStylesheet.marginRight) + type(ButtonType.Button) + onClick { viewModel.onEvent(CategoryCreateEvent.OnCancel) } + } + ) { + Span(attrs = { classes("mdc-button__label") } + ) { Text("Cancel") } + } + SubmitInput( + attrs = { + classes("mdc-button", "mdc-button--raised") + value("Submit") + }) + + } + } +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryDetailView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryDetailView.kt new file mode 100644 index 00000000..c26e8829 --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryDetailView.kt @@ -0,0 +1,36 @@ +package de.hsfl.budgetBinder.screens.category + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import de.hsfl.budgetBinder.compose.CategoryDetailed +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import de.hsfl.budgetBinder.presentation.event.LifecycleEvent +import de.hsfl.budgetBinder.presentation.viewmodel.category.detail.CategoryDetailEvent +import de.hsfl.budgetBinder.presentation.viewmodel.category.detail.CategoryDetailViewModel +import di +import org.jetbrains.compose.web.dom.H1 +import org.jetbrains.compose.web.dom.Text +import org.kodein.di.instance + +@Composable +fun CategoryDetailView() { + val viewModel: CategoryDetailViewModel by di.instance() + val category by viewModel.categoryState.collectAsState() + + //LifeCycle + LaunchedEffect(key1 = true) { + viewModel.onEvent(CategoryDetailEvent.LifeCycle(LifecycleEvent.OnLaunch)) + } + + //Webpage Content + H1(attrs = { classes(AppStylesheet.h1) }) { Text(" Category Detailed") } + CategoryDetailed( + category, + { viewModel.onEvent(CategoryDetailEvent.OnEdit) }, + { viewModel.onEvent(CategoryDetailEvent.OnDelete) }, + { viewModel.onEvent(CategoryDetailEvent.OnBack) } + ) + +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryEditView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryEditView.kt new file mode 100644 index 00000000..b0be876e --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategoryEditView.kt @@ -0,0 +1,210 @@ +package de.hsfl.budgetBinder.screens.category + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.compose.CategoryImagesToImageList +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import de.hsfl.budgetBinder.presentation.event.LifecycleEvent +import de.hsfl.budgetBinder.presentation.viewmodel.category.edit.CategoryEditEvent +import de.hsfl.budgetBinder.presentation.viewmodel.category.edit.CategoryEditViewModel +import di +import org.jetbrains.compose.web.attributes.* +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.* +import org.kodein.di.instance + + +@Composable +fun CategoryEditView() { + val viewModel: CategoryEditViewModel by di.instance() + val categoryNameState by viewModel.categoryNameState.collectAsState() + val categoryColorState by viewModel.categoryColorState.collectAsState() + val categoryImageState by viewModel.categoryImageState.collectAsState() + val categoryBudgetState by viewModel.categoryBudgetState.collectAsState() + + //Life Cycle + LaunchedEffect(Unit) { + viewModel.onEvent(CategoryEditEvent.LifeCycle(LifecycleEvent.OnLaunch)) + } + DisposableEffect(Unit) { + onDispose { + viewModel.onEvent(CategoryEditEvent.LifeCycle(LifecycleEvent.OnDispose)) + } + } + + //Webpage Content + H1(attrs = { classes(AppStylesheet.h1) }) { Text("Edit Category") } + + Form(attrs = { + this.addEventListener("submit") { + viewModel.onEvent(CategoryEditEvent.OnSave) + it.preventDefault() + } + } + ) { + //Category Name Input + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled") + classes(AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Category Name") } + Input( + type = InputType.Text + ) { + classes("mdc-text-field__input") + value(categoryNameState) + required(true) + onInput { + viewModel.onEvent(CategoryEditEvent.EnteredCategoryName(it.value)) + } + } + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + //Category Color Input + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--outlined") + classes(AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + style { marginBottom(1.percent) } + } + ) { Text("Color") } + Input( + type = InputType.Color + ) { + classes("mdc-text-field__input") + value("#$categoryColorState") + onInput { + viewModel.onEvent(CategoryEditEvent.EnteredCategoryColor(it.value.drop(1))) + } + } + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + //Category Image Input + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes(AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + style { marginBottom(1.percent); marginLeft(2.percent) } + } + ) { Text("Image") } + CategoryImagesToImageList( + categoryImageState, + onClick = { viewModel.onEvent(CategoryEditEvent.EnteredCategoryImage(it)) }) + } + } + //Category Budget Input + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled") + classes(AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Budget") } + Input( + type = InputType.Number + ) { + classes("mdc-text-field__input") + value(categoryBudgetState) + attr("step", "0.01") + required(true) + min("1") + onInput { + viewModel.onEvent(CategoryEditEvent.EnteredCategoryBudget(it.value as Float)) + } + } + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + + //Submit button + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Button( + attrs = { + classes("mdc-button", "mdc-button--raised", AppStylesheet.marginRight) + type(ButtonType.Button) + onClick { viewModel.onEvent(CategoryEditEvent.OnCancel) } + } + ) { + Span(attrs = { classes("mdc-button__label") } + ) { Text("Cancel") } + } + SubmitInput( + attrs = { + classes("mdc-button", "mdc-button--raised") + value("Submit") + }) + + } + } +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategorySummaryView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategorySummaryView.kt new file mode 100644 index 00000000..1c3efb7e --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/category/CategorySummaryView.kt @@ -0,0 +1,42 @@ +package de.hsfl.budgetBinder.screens.category + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.compose.CategoryList +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import de.hsfl.budgetBinder.presentation.event.LifecycleEvent +import de.hsfl.budgetBinder.presentation.viewmodel.category.summary.CategorySummaryEvent +import de.hsfl.budgetBinder.presentation.viewmodel.category.summary.CategorySummaryViewModel +import di +import kotlinx.coroutines.flow.collectLatest +import org.jetbrains.compose.web.css.margin +import org.jetbrains.compose.web.css.percent +import org.jetbrains.compose.web.dom.* +import org.kodein.di.instance + + +@Composable +fun CategorySummaryView( +) { + val viewModel: CategorySummaryViewModel by di.instance() + val categoryList by viewModel.categoryList.collectAsState() + + //LifeCycle + LaunchedEffect(key1 = true) { + viewModel.onEvent(CategorySummaryEvent.LifeCycle(LifecycleEvent.OnLaunch)) + viewModel.eventFlow.collectLatest {} + } + + //Webpage Content + H1(attrs = { classes(AppStylesheet.h1) }) { Text(" Category Summary") } + Button(attrs = { + classes("mdc-button", "mdc-button--raised") + onClick { viewModel.onEvent(CategorySummaryEvent.OnCategoryCreate) } + style { margin(2.percent) } + }) { + Text("Create Category") + } + CategoryList( + categoryList + ) { id -> viewModel.onEvent(CategorySummaryEvent.OnCategory(id)) } +} + diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/dashboard/DashboardComponent.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/dashboard/DashboardComponent.kt new file mode 100644 index 00000000..dad3891f --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/dashboard/DashboardComponent.kt @@ -0,0 +1,237 @@ +package de.hsfl.budgetBinder.screens.dashboard + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.common.Category +import de.hsfl.budgetBinder.compose.BudgetBar +import de.hsfl.budgetBinder.compose.Icon +import de.hsfl.budgetBinder.compose.MainFlexContainer +import de.hsfl.budgetBinder.compose.NavBar +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import de.hsfl.budgetBinder.presentation.CategoryImageToIcon +import de.hsfl.budgetBinder.presentation.event.LifecycleEvent +import de.hsfl.budgetBinder.presentation.event.UiEvent +import de.hsfl.budgetBinder.presentation.viewmodel.dashboard.DashboardEntryState +import de.hsfl.budgetBinder.presentation.viewmodel.dashboard.DashboardEvent +import de.hsfl.budgetBinder.presentation.viewmodel.dashboard.DashboardState +import de.hsfl.budgetBinder.presentation.viewmodel.dashboard.DashboardViewModel +import di +import kotlinx.coroutines.flow.collectLatest +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.* +import org.kodein.di.instance +import kotlin.math.absoluteValue + +@Composable +fun DashboardComponent() { + val viewModel: DashboardViewModel by di.instance() + val entryList = viewModel.entryListState.collectAsState() + val focusedCategory = viewModel.focusedCategoryState.collectAsState() + val totalSpendBudget = viewModel.spendBudgetOnCurrentCategory.collectAsState() + val olderEntries = viewModel.oldEntriesMapState.collectAsState() + val loadingState = remember { mutableStateOf(false) } + + + //LifeCycle + LaunchedEffect(key1 = true) { + viewModel.onEvent(DashboardEvent.LifeCycle(LifecycleEvent.OnLaunch)) + viewModel.eventFlow.collectLatest { event -> + when (event) { + is UiEvent.ShowLoading -> loadingState.value = true + is UiEvent.HideSuccess -> loadingState.value = false + else -> loadingState.value = false + } + } + } + DisposableEffect(Unit) { + onDispose { + viewModel.onEvent(DashboardEvent.LifeCycle(LifecycleEvent.OnDispose)) + } + } + + //Webpage content + NavBar {} + MainFlexContainer { + Div { + DashboardData( + focusedCategory = focusedCategory.value.category, + totalSpendBudget = totalSpendBudget.value.spendBudgetOnCurrentCategory, + totalBudget = focusedCategory.value.category.budget, + hasPrev = focusedCategory.value.hasPrev, + hasNext = focusedCategory.value.hasNext, + onPrevClicked = { viewModel.onEvent(DashboardEvent.OnPrevCategory) }, + onNextClicked = { viewModel.onEvent(DashboardEvent.OnNextCategory) } + ) + EntryList(entryList = entryList.value.entryList, + oldEntries = olderEntries.value, + onItemClicked = { viewModel.onEvent(DashboardEvent.OnEntry(it)) }, + onLoadMore = { viewModel.onEvent(DashboardEvent.OnLoadMore) }, + onEntryDelete = { viewModel.onEvent(DashboardEvent.OnEntryDelete(it)) } + ) + } + CreateNewEntryButton { viewModel.onEvent(DashboardEvent.OnEntryCreate) } + } +} + +@Composable +fun DashboardData( + focusedCategory: Category, + totalSpendBudget: Float, + totalBudget: Float, + hasPrev: Boolean, + hasNext: Boolean, + onPrevClicked: () -> Unit, + onNextClicked: () -> Unit +) { + SwipeContainer( + hasPrev, hasNext, onPrevClicked, onNextClicked + ) { + BudgetBar(focusedCategory, totalSpendBudget, totalBudget) + } +} + + +@Composable +fun CreateNewEntryButton(onEntryCreateButton: () -> Unit) { + Div(attrs = { + style { + display(DisplayStyle.Flex) + justifyContent(JustifyContent.FlexEnd) + } + }) { + Button(attrs = { + classes("mdc-fab", "mdc-fab--touch", AppStylesheet.newEntryButton) + onClick { onEntryCreateButton() } + }) { + Div(attrs = { classes("mdc-fab__ripple") }) + Icon("add") + Div(attrs = { classes("mdc-fab__touch") }) + } + } +} + +@Composable +fun SwipeContainer( + hasPrev: Boolean, + hasNext: Boolean, + onPrevClicked: () -> Unit, + onNextClicked: () -> Unit, + content: @Composable () -> Unit +) { + Div( + attrs = { + classes(AppStylesheet.flexContainer) + }) { + Div(attrs = { + if (hasPrev) { + classes(AppStylesheet.imageFlexContainer, "mdc-button") + onClick { onPrevClicked() } + } else { + classes(AppStylesheet.imageFlexContainer) + style { + paddingLeft(8.px) + paddingRight(8.px) + } + } + }) { + if (hasPrev) Icon("arrow_back_ios_new") + } + Div(attrs = { classes(AppStylesheet.budgetBarContainer) }) + { + content() + } + Div(attrs = { + if (hasNext) { + classes(AppStylesheet.imageFlexContainer, "mdc-button") + onClick { onNextClicked() } + } else { + classes(AppStylesheet.imageFlexContainer) + style { + paddingLeft(8.px) + paddingRight(8.px) + } + } + }) { + if (hasNext) Icon("arrow_forward_ios_new") + } + } +} + +//TODO: Load Old Data and old Entries +@Composable +fun EntryList( + entryList: List, + oldEntries: Map, + onItemClicked: (Int) -> Unit, + onLoadMore: () -> Unit, + onEntryDelete: (Int) -> Unit + +) { + if (entryList.isEmpty()) { + Div(attrs = { + classes( + "mdc-typography--headline5", + AppStylesheet.text + ) + }) { Text("This category has no entries. You can create an new entry.") } + } else { + for (entry in entryList) { + EntryListElement(entry, onItemClicked, onEntryDelete) + } + Text("Older entries...") + for ((date, dashboardState) in oldEntries) { + Text(date) //TODO-WEB: Sticky? + for (entry in dashboardState.entryList) { + EntryListElement(entry, onItemClicked, onEntryDelete) + } + } + Button( + attrs = { + classes("mdc-button", "mdc-button--raised", "mdc-top-app-bar__navigation-icon") + onClick { onLoadMore() } + } + ) { + Span(attrs = { classes("mdc-button__label") } + ) { Text("Load more Entries") } + } + } +} + + +@Composable +fun EntryListElement( + entry: DashboardEntryState, + onItemClicked: (Int) -> Unit, + onEntryDelete: (Int) -> Unit +) { + Div(attrs = { + classes("mdc-card", "mdc-card--outlined", AppStylesheet.entryListElement) + onClick { onItemClicked(entry.entry.id) } + }) { + CategoryImageToIcon(entry.categoryImage) + Div(attrs = { classes(AppStylesheet.entryListElementText) }) { + Div(attrs = { + classes( + "mdc-typography--headline5", + AppStylesheet.text + ) + }) { Text(entry.entry.name) } + } + Div(attrs = { classes(AppStylesheet.imageFlexContainer) }) { + Div(attrs = { + classes( + "mdc-typography--headline5", + AppStylesheet.moneyText + ) + }) { Text(amountToString(entry.entry.amount)) } + } + } +} + +fun amountToString(amount: Float): String { + //This whole thing just so it's "- 10 €" and not "-10 €" + val x = if (amount < 0) "-" else "" + return "$x ${amount.absoluteValue} €" +} + + + diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/entry/EntryComponent.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/entry/EntryComponent.kt new file mode 100644 index 00000000..a2915e71 --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/entry/EntryComponent.kt @@ -0,0 +1,52 @@ +package de.hsfl.budgetBinder.screens.entry + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.compose.MainFlexContainer +import de.hsfl.budgetBinder.compose.NavBar +import de.hsfl.budgetBinder.presentation.Screen +import de.hsfl.budgetBinder.presentation.event.LifecycleEvent +import de.hsfl.budgetBinder.presentation.event.UiEvent +import de.hsfl.budgetBinder.presentation.flow.RouterFlow +import de.hsfl.budgetBinder.presentation.viewmodel.category.edit.CategoryEditEvent +import de.hsfl.budgetBinder.presentation.viewmodel.entry.EntryEvent +import de.hsfl.budgetBinder.presentation.viewmodel.entry.EntryViewModel +import di +import kotlinx.coroutines.flow.collectLatest +import org.kodein.di.instance + +@Composable +fun EntryComponent() { + + val viewModel: EntryViewModel by di.instance() + val routerFlow: RouterFlow by di.instance() + val screenState = routerFlow.state.collectAsState() + + + //Webpage content + NavBar {} + MainFlexContainer { + when (screenState.value) { + is Screen.Entry.Create -> { + EntryCreateView( + onCreateButton = { viewModel.onEvent(EntryEvent.OnCreateEntry) } + ) + } + is Screen.Entry.Overview -> { + EntryOverviewView( + onEditButton = { viewModel.onEvent(EntryEvent.OnEditEntry) }, + onDeleteButton = { viewModel.onEvent(EntryEvent.OnDeleteEntry) }, + onDeleteDialogConfirmButton = { viewModel.onEvent(EntryEvent.OnDeleteDialogConfirm) }, + onDeleteDialogDismissButton = { viewModel.onEvent(EntryEvent.OnDeleteDialogDismiss) }, + onCancel = { viewModel.onEvent(EntryEvent.OnCancel) } + ) + } + is Screen.Entry.Edit -> { + EntryEditView( + onEditButton = { viewModel.onEvent(EntryEvent.OnEditEntry) } + ) + } + else -> { + } + } + } +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/entry/EntryCreateView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/entry/EntryCreateView.kt new file mode 100644 index 00000000..c92de940 --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/entry/EntryCreateView.kt @@ -0,0 +1,240 @@ +package de.hsfl.budgetBinder.screens.entry + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.compose.* +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import de.hsfl.budgetBinder.presentation.event.LifecycleEvent +import de.hsfl.budgetBinder.presentation.event.UiEvent +import de.hsfl.budgetBinder.presentation.viewmodel.entry.EntryEvent +import de.hsfl.budgetBinder.presentation.viewmodel.entry.EntryViewModel +import di +import kotlinx.coroutines.flow.collectLatest +import org.jetbrains.compose.web.ExperimentalComposeWebSvgApi +import org.jetbrains.compose.web.attributes.* +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.* +import org.jetbrains.compose.web.svg.Path +import org.jetbrains.compose.web.svg.Svg +import org.kodein.di.instance + + +@OptIn(ExperimentalComposeWebSvgApi::class) +@Composable +fun EntryCreateView( + onCreateButton: () -> Unit, +) { + val viewModel: EntryViewModel by di.instance() + //Input + val entryNameTextField by viewModel.nameText.collectAsState() + val entryAmountTextField by viewModel.amountText.collectAsState() + val entryCategoryIDTextField by viewModel.categoryIDState.collectAsState() + val amountSign by viewModel.amountSignState.collectAsState() + //Data + val categoryList by viewModel.categoryListState.collectAsState() + + //LifeCycle + LaunchedEffect(Unit) { + viewModel.onEvent(EntryEvent.LifeCycle(LifecycleEvent.OnLaunch)) + } + DisposableEffect(Unit) { + onDispose { + viewModel.onEvent(EntryEvent.LifeCycle(LifecycleEvent.OnDispose)) + } + } + + //Webpage Content + + H1(attrs = { classes(AppStylesheet.h1) }) { Text("Create new Entry") } + Form(attrs = { + this.addEventListener("submit") { + onCreateButton() + it.preventDefault() + } + } + ) { + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Entry Name") } + Input( + type = InputType.Text + ) { + classes("mdc-text-field__input") + value(entryNameTextField) + required(true) + onInput { + viewModel.onEvent(EntryEvent.EnteredName(it.value)) + } + } + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--outlined", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + style { marginBottom(1.percent) } + } + ) { Text("Amount") } + Div { + Button( + attrs = { + if (!amountSign) classes("mdc-switch", "mdc-switch--unselected") + else classes("mdc-switch", "mdc-switch--selected") + id("basic-switch") + attr("role", "switch") + attr("aria-checked", "false") + type(ButtonType.Button) + onClick { viewModel.onEvent(EntryEvent.EnteredAmountSign) } + } + ) { + Div(attrs = { classes("mdc-switch__track") }) { } + Div(attrs = { classes("mdc-switch__handle-track") }) { + Div(attrs = { classes("mdc-switch__handle") }) { + Div(attrs = { classes("mdc-switch__shadow") }) { + Div(attrs = { classes("mdc-elevation-overlay") }) { } + } + Div(attrs = { classes("mdc-switch__ripple") }) { } + Div(attrs = { classes("mdc-switch__icons") }) { + Svg( + attrs = { classes("mdc-switch__icon", "mdc-switch__icon") }, + viewBox = "0 0 24 24" + ) { + Path("M19.69,5.23L8.96,15.96l-4.23-4.23L2.96,13.5l6,6L21.46,7L19.69,5.23z") + } + Svg( + attrs = { classes("mdc-switch__icon", "mdc-switch__icon") }, + viewBox = "0 0 24 24" + ) { + Path("M20 13H4v-2h16v2z") + } + } + } + } + } + } + Div(attrs = { + classes("mdc-typography--headline6", AppStylesheet.text) + }) { + Text(if (amountSign) "+" else "-") + } + Input( + type = InputType.Number + ) { + attr("step", "0.01") + value(entryAmountTextField) + classes("mdc-text-field__input") + onInput { + viewModel.onEvent(EntryEvent.EnteredAmount(it.value!!.toFloat())) + } + } + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin, AppStylesheet.flexContainer) + } + ) { + Div(attrs = { classes(AppStylesheet.flex50) }) { + Div(attrs = { classes("mdc-form-field") }) { + Div(attrs = { classes("mdc-checkbox") }) { + Input(type = InputType.Checkbox) + { + classes("mdc-checkbox__native-control") + id("checkbox-1") + onInput { + viewModel.onEvent(EntryEvent.EnteredRepeat) + } + } + Div(attrs = { classes("mdc-checkbox__background") }) { + Svg( + attrs = { classes("mdc-checkbox__checkmark") }, + viewBox = "0 0 24 24" + ) { + Path( + "M1.73,12.91 8.1,19.28 22.79,4.59", + attrs = { classes("mdc-checkbox__checkmark") }) + } + Div(attrs = { classes("mdc-checkbox__mixedmark") }) { } + } + Div(attrs = { classes("mdc-checkbox__ripple") }) { } + } + Label(forId = "checkbox-1") { Text("repeat") } + } + } + Div(attrs = { classes(AppStylesheet.flex50) }) { + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Text("Category: ") + ChooseCategoryMenu(categoryList, entryCategoryIDTextField) { id -> + viewModel.onEvent(EntryEvent.EnteredCategoryID(id)) + } + } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Button( + attrs = { + classes("mdc-button", "mdc-button--raised", AppStylesheet.marginRight) + type(ButtonType.Button) + onClick { viewModel.onEvent(EntryEvent.OnCancel) } + } + ) { + Span(attrs = { classes("mdc-button__label") } + ) { Text("Cancel") } + } + SubmitInput( + attrs = { + classes("mdc-button", "mdc-button--raised") + value("Submit") + }) + } + } +} + diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/entry/EntryEditView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/entry/EntryEditView.kt new file mode 100644 index 00000000..8d9f4a73 --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/entry/EntryEditView.kt @@ -0,0 +1,241 @@ +package de.hsfl.budgetBinder.screens.entry + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.compose.ChooseCategoryMenu +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import de.hsfl.budgetBinder.presentation.event.LifecycleEvent +import de.hsfl.budgetBinder.presentation.viewmodel.entry.EntryEvent +import de.hsfl.budgetBinder.presentation.viewmodel.entry.EntryViewModel +import di +import org.jetbrains.compose.web.ExperimentalComposeWebSvgApi +import org.jetbrains.compose.web.attributes.ButtonType +import org.jetbrains.compose.web.attributes.InputType +import org.jetbrains.compose.web.attributes.required +import org.jetbrains.compose.web.attributes.type +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.* +import org.jetbrains.compose.web.svg.Path +import org.jetbrains.compose.web.svg.Svg +import org.kodein.di.instance + + +@OptIn(ExperimentalComposeWebSvgApi::class) +@Composable +fun EntryEditView( + onEditButton: () -> Unit, +) { + val viewModel: EntryViewModel by di.instance() + //Input + val entryNameTextField by viewModel.nameText.collectAsState() + val entryAmountTextField by viewModel.amountText.collectAsState() + val entryRepeat by viewModel.repeatState.collectAsState() + val entryCategoryIDTextField by viewModel.categoryIDState.collectAsState() + val amountSign by viewModel.amountSignState.collectAsState() + //Data + val categoryList by viewModel.categoryListState.collectAsState() + + //LifeCycle + LaunchedEffect(Unit) { + viewModel.onEvent(EntryEvent.LifeCycle(LifecycleEvent.OnLaunch)) + } + DisposableEffect(Unit) { + onDispose { + viewModel.onEvent(EntryEvent.LifeCycle(LifecycleEvent.OnDispose)) + } + } + + //Webpage Content + H1(attrs = { classes(AppStylesheet.h1) }) { Text("Edit Entry") } + Form(attrs = { + this.addEventListener("submit") { + onEditButton() + it.preventDefault() + } + } + ) { + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Entry Name") } + Input( + type = InputType.Text + ) { + classes("mdc-text-field__input") + value(entryNameTextField) + required(true) + onInput { + viewModel.onEvent(EntryEvent.EnteredName(it.value)) + } + } + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--outlined", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + style { marginBottom(1.percent) } + } + ) { Text("Amount") } + Div { + Button( + attrs = { + if (!amountSign) classes("mdc-switch", "mdc-switch--unselected") + else classes("mdc-switch", "mdc-switch--selected") + id("basic-switch") + attr("role", "switch") + attr("aria-checked", "false") + type(ButtonType.Button) + onClick { viewModel.onEvent(EntryEvent.EnteredAmountSign) } + } + ) { + Div(attrs = { classes("mdc-switch__track") }) { } + Div(attrs = { classes("mdc-switch__handle-track") }) { + Div(attrs = { classes("mdc-switch__handle") }) { + Div(attrs = { classes("mdc-switch__shadow") }) { + Div(attrs = { classes("mdc-elevation-overlay") }) { } + } + Div(attrs = { classes("mdc-switch__ripple") }) { } + Div(attrs = { classes("mdc-switch__icons") }) { + Svg( + attrs = { classes("mdc-switch__icon", "mdc-switch__icon") }, + viewBox = "0 0 24 24" + ) { + Path("M19.69,5.23L8.96,15.96l-4.23-4.23L2.96,13.5l6,6L21.46,7L19.69,5.23z") + } + Svg( + attrs = { classes("mdc-switch__icon", "mdc-switch__icon") }, + viewBox = "0 0 24 24" + ) { + Path("M20 13H4v-2h16v2z") + } + } + } + } + } + } + Div(attrs = { + classes("mdc-typography--headline6", AppStylesheet.text) + }) { + Text(if (amountSign) "+" else "-") + } + Input( + type = InputType.Number + ) { + attr("step", "0.01") + classes("mdc-text-field__input") + value(entryAmountTextField) + onInput { + viewModel.onEvent(EntryEvent.EnteredAmount(it.value as Float)) + } + } + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin, AppStylesheet.flexContainer) + } + ) { + Div(attrs = { classes(AppStylesheet.flex50) }) { + Div(attrs = { classes("mdc-form-field") }) { + Div(attrs = { classes("mdc-checkbox") }) { + CheckboxInput(attrs = + { + checked(entryRepeat) + classes("mdc-checkbox__native-control") + id("checkbox-1") + onInput { + viewModel.onEvent(EntryEvent.EnteredRepeat) + } + }) + Div(attrs = { classes("mdc-checkbox__background") }) { + Svg( + attrs = { classes("mdc-checkbox__checkmark") }, + viewBox = "0 0 24 24" + ) { + Path( + "M1.73,12.91 8.1,19.28 22.79,4.59", + attrs = { classes("mdc-checkbox__checkmark") }) + } + Div(attrs = { classes("mdc-checkbox__mixedmark") }) { } + } + Div(attrs = { classes("mdc-checkbox__ripple") }) { } + } + Label(forId = "checkbox-1") { Text("repeat") } + } + } + Div(attrs = { classes(AppStylesheet.flex50) }) { + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Text("Category: ") + ChooseCategoryMenu(categoryList, entryCategoryIDTextField) { id -> + viewModel.onEvent(EntryEvent.EnteredCategoryID(id)) + } + } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Button( + attrs = { + classes("mdc-button", "mdc-button--raised", AppStylesheet.marginRight) + type(ButtonType.Button) + onClick { viewModel.onEvent(EntryEvent.OnCancel) } + } + ) { + Span(attrs = { classes("mdc-button__label") } + ) { Text("Cancel") } + } + SubmitInput( + attrs = { + classes("mdc-button", "mdc-button--raised") + value("Submit") + }) + } + } +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/entry/EntryOverviewView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/entry/EntryOverviewView.kt new file mode 100644 index 00000000..0bc7fffc --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/entry/EntryOverviewView.kt @@ -0,0 +1,143 @@ +package de.hsfl.budgetBinder.screens.entry + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.common.Category +import de.hsfl.budgetBinder.common.Entry +import de.hsfl.budgetBinder.compose.DeleteDialog +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import de.hsfl.budgetBinder.presentation.CategoryImageToIcon +import de.hsfl.budgetBinder.presentation.event.LifecycleEvent +import de.hsfl.budgetBinder.presentation.viewmodel.entry.EntryEvent +import de.hsfl.budgetBinder.presentation.viewmodel.entry.EntryViewModel +import di +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.* +import org.kodein.di.instance + + +@Composable +fun EntryOverviewView( + onEditButton: () -> Unit, + onDeleteButton: () -> Unit, + onDeleteDialogConfirmButton: () -> Unit, + onDeleteDialogDismissButton: () -> Unit, + onCancel: () -> Unit +) { + val viewModel: EntryViewModel by di.instance() + //Data + val entry by viewModel.selectedEntryState.collectAsState() + val deleteDialog by viewModel.dialogState.collectAsState() + val categoryList by viewModel.categoryListState.collectAsState() + + //LifeCycle + LaunchedEffect(Unit) { + viewModel.onEvent(EntryEvent.LifeCycle(LifecycleEvent.OnLaunch)) + } + DisposableEffect(Unit) { + onDispose { + viewModel.onEvent(EntryEvent.LifeCycle(LifecycleEvent.OnDispose)) + } + } + + //Webpage Content + H1(attrs = { classes(AppStylesheet.h1) }) { Text(" Entry") } + EntryOverview( + entry, + categoryList, + deleteDialog, + onEditButton, + onDeleteButton, + onDeleteDialogConfirmButton, + onDeleteDialogDismissButton, + onCancel + ) +} + +@Composable +fun EntryOverview( + entry: Entry, + categoryList: List, + deleteDialog: Boolean, + onEditButton: () -> Unit, + onDeleteButton: () -> Unit, + onDeleteDialogConfirmButton: () -> Unit, + onDeleteDialogDismissButton: () -> Unit, + onCancel: () -> Unit +) { + Div( + attrs = { + classes(AppStylesheet.categoryListElement, AppStylesheet.flexContainer) + } + ) { + + Div( + attrs = { + classes(AppStylesheet.categoryListElementText) + } + ) { + Div { + Div(attrs = { + classes("mdc-typography--headline4", AppStylesheet.text) + }) { Text(entry.name) } + Div(attrs = { classes(AppStylesheet.flexContainer) }) { + Div(attrs = { + classes("mdc-typography--headline6", AppStylesheet.text, AppStylesheet.buttonOverview) + }) { + var categoryName = "No Category" + var categoryIcon = Category.Image.DEFAULT + for (category in categoryList) { + if (entry.category_id == category.id) { + categoryName = category.name + categoryIcon = category.image + } + } + Text("Category: $categoryName") + CategoryImageToIcon(categoryIcon) + } + Div(attrs = { + classes("mdc-typography--headline6", AppStylesheet.text, AppStylesheet.buttonOverview) + }) { Text("Amount: ${entry.amount}€") } + Div(attrs = { + classes("mdc-typography--headline6", AppStylesheet.text, AppStylesheet.buttonOverview) + }) { Text("Repeat: " + if (entry.repeat) "Yes" else "No") } + } + } + } + } + Div( + attrs = { + classes(AppStylesheet.flexContainer) + } + ) { + Button(attrs = { + classes("mdc-button", "mdc-button--raised", AppStylesheet.buttonOverview) + onClick { onCancel() } + } + ) { + Span(attrs = { classes("mdc-button__label") } + ) { Text("Cancel") } + } + Button(attrs = { + classes("mdc-button", "mdc-button--raised", AppStylesheet.buttonOverview) + onClick { onEditButton() } + }) { + Text("Edit Entry") + } + Button(attrs = { + classes("mdc-button", "mdc-button--raised", AppStylesheet.buttonOverview) + onClick { onDeleteButton() } + style { backgroundColor(Color("#b00020")) } + }) { + Text("Delete Entry") + } + } + if (deleteDialog) { + DeleteDialog( + false, + { onDeleteDialogConfirmButton() }, + { onDeleteDialogDismissButton() }) + { Text("Delete Entry?") } + } +} + + diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/login/LoginComponent.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/login/LoginComponent.kt new file mode 100644 index 00000000..d1253c2b --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/login/LoginComponent.kt @@ -0,0 +1,195 @@ +package de.hsfl.budgetBinder.screens.login + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.compose.MainFlexContainer +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import de.hsfl.budgetBinder.presentation.event.LifecycleEvent +import de.hsfl.budgetBinder.presentation.event.UiEvent +import de.hsfl.budgetBinder.presentation.viewmodel.auth.login.LoginEvent +import de.hsfl.budgetBinder.presentation.viewmodel.auth.login.LoginViewModel +import di +import kotlinx.coroutines.flow.collectLatest +import org.jetbrains.compose.web.css.percent +import org.jetbrains.compose.web.css.width +import org.jetbrains.compose.web.dom.* +import org.kodein.di.instance + + +@Composable +fun LoginComponent() { + val scope = rememberCoroutineScope() + val viewModel: LoginViewModel by di.instance() + val emailTextState = viewModel.emailText.collectAsState(scope.coroutineContext) + val passwordTextState = viewModel.passwordText.collectAsState(scope.coroutineContext) + val loadingState = remember { mutableStateOf(false) } + + + //LifeCycle + LaunchedEffect(key1 = true) { + viewModel.onEvent(LoginEvent.LifeCycle(LifecycleEvent.OnLaunch)) + viewModel.eventFlow.collectLatest { event -> + when (event) { + is UiEvent.ShowLoading -> loadingState.value = true + else -> loadingState.value = false + } + } + } + DisposableEffect(Unit) { + onDispose { + viewModel.onEvent( + LoginEvent.LifeCycle(LifecycleEvent.OnDispose) + ) + } + } + + //Webpage content + Header( + attrs = { + classes("mdc-top-app-bar") + } + ) { + Div( + attrs = { + classes("mdc-top-app-bar__row") + } + ) { + Section( + attrs = { + classes("mdc-top-app-bar__section", "mdc-top-app-bar__section--align-start") + } + ) { + Img( + src = "images/Logo.png", alt = "Logo", attrs = { + classes("mdc-icon-button", AppStylesheet.image) + } + ) + Span( + attrs = { + classes("mdc-top-app-bar__title") + } + ) { + Text("Budget-Binder") + } + } + Section( + attrs = { + classes("mdc-top-app-bar__section", "mdc-top-app-bar__section--align-end") + } + ) { + Button( + attrs = { + classes( + "mdc-button", + "mdc-button--raised", + "mdc-top-app-bar__navigation-icon" + ) + onClick { viewModel.onEvent(LoginEvent.OnRegisterScreen) } + } + ) { + Span( + attrs = { + classes("mdc-button__label") + } + ) { + Text("Register Instead") + } + } + } + } + } + + MainFlexContainer { + // -- Login Form -- + H1(attrs = { classes(AppStylesheet.h1) }) { Text("Login") } + Form( + attrs = { + this.addEventListener("submit") { + console.log("$emailTextState, $passwordTextState") + viewModel.onEvent(LoginEvent.OnServerUrlDialogConfirm) //TODO: Change to OnLogin as soon as LoginViewModel has corresponding logic + it.preventDefault() + } + } + ) { + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Email") } + EmailInput(value = emailTextState.value.email, + attrs = { + classes("mdc-text-field__input") + onInput { + viewModel.onEvent(LoginEvent.EnteredEmail(it.value)) + + } + }) + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Password") } + PasswordInput(value = passwordTextState.value.password, + attrs = { + classes("mdc-text-field__input") + onInput { + viewModel.onEvent(LoginEvent.EnteredPassword(it.value)) + } + }) + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + SubmitInput( + attrs = { + classes("mdc-button", "mdc-button--raised") + value("Submit") + }) + } + } + } +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/register/RegisterComponent.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/register/RegisterComponent.kt new file mode 100644 index 00000000..68b9b54d --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/register/RegisterComponent.kt @@ -0,0 +1,319 @@ +package de.hsfl.budgetBinder.screens.register + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.compose.FeedbackSnackbar +import de.hsfl.budgetBinder.compose.MainFlexContainer +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import de.hsfl.budgetBinder.presentation.event.LifecycleEvent +import de.hsfl.budgetBinder.presentation.event.UiEvent +import de.hsfl.budgetBinder.presentation.viewmodel.auth.register.RegisterEvent +import de.hsfl.budgetBinder.presentation.viewmodel.auth.register.RegisterViewModel +import di +import kotlinx.coroutines.flow.collectLatest +import org.jetbrains.compose.web.attributes.InputType +import org.jetbrains.compose.web.attributes.required +import org.jetbrains.compose.web.css.marginLeft +import org.jetbrains.compose.web.css.percent +import org.jetbrains.compose.web.css.width +import org.jetbrains.compose.web.dom.* +import org.kodein.di.instance + + +@Composable +fun RegisterComponent() { + val viewModel: RegisterViewModel by di.instance() + val firstNameTextState = viewModel.firstNameText.collectAsState() + val lastNameTextState = viewModel.lastNameText.collectAsState() + val emailTextState = viewModel.emailText.collectAsState() + val passwordTextState = viewModel.passwordText.collectAsState() + val confirmedPasswordTextState = viewModel.confirmedPasswordText.collectAsState() + val loadingState = remember { mutableStateOf(false) } + var openSnackbar by remember { mutableStateOf(false) } + + + //LifeCycle + LaunchedEffect(key1 = true) { + viewModel.onEvent(RegisterEvent.LifeCycle(LifecycleEvent.OnLaunch)) + viewModel.eventFlow.collectLatest { event -> + when (event) { + is UiEvent.ShowLoading -> { + // TODO: Refactor this, it's working but ahh + loadingState.value = true + } + else -> loadingState.value = false + } + } + } + DisposableEffect(Unit) { + onDispose { + viewModel.onEvent(RegisterEvent.LifeCycle(LifecycleEvent.OnDispose)) + } + } + + //Webpage content + Header( + attrs = { + classes("mdc-top-app-bar") + } + ) { + Div( + attrs = { + classes("mdc-top-app-bar__row") + } + ) { + Section( + attrs = { + classes("mdc-top-app-bar__section", "mdc-top-app-bar__section--align-start") + } + ) { + Img( + src = "images/Logo.png", alt = "Logo", attrs = { + classes("mdc-icon-button", AppStylesheet.image) + } + ) + Span( + attrs = { + classes("mdc-top-app-bar__title") + } + ) { + Text("Budget-Binder") + } + } + Section( + attrs = { + classes("mdc-top-app-bar__section", "mdc-top-app-bar__section--align-end") + } + ) { + Button( + attrs = { + classes( + "mdc-button", + "mdc-button--raised", + "mdc-top-app-bar__navigation-icon" + ) + onClick { viewModel.onEvent(RegisterEvent.OnLoginScreen) } + } + ) { + Span( + attrs = { + classes("mdc-button__label") + } + ) { + Text("Login Instead") + } + } + } + } + } + + MainFlexContainer { + // -- Register Form -- + H1(attrs = { classes(AppStylesheet.h1) }) { Text("Register") } + Form(attrs = { //Probably possible with just a button OnClick instead of Form&Submit + this.addEventListener("submit") { + console.log("$firstNameTextState, $lastNameTextState, $emailTextState, $passwordTextState") + if (!confirmedPasswordTextState.value.confirmedPasswordValid) { + openSnackbar = true + } + viewModel.onEvent(RegisterEvent.OnRegister) + it.preventDefault() + } + } + ) { + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Firstname") } + Input( + type = InputType.Text + ) { + classes("mdc-text-field__input") + required() + value(firstNameTextState.value.firstName) + onInput { + viewModel.onEvent(RegisterEvent.EnteredFirstname(it.value)) + } + } + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Lastname") } + Input( + type = InputType.Text + ) { + classes("mdc-text-field__input") + required() + value(lastNameTextState.value.lastName) + onInput { + viewModel.onEvent(RegisterEvent.EnteredLastname(it.value)) + } + } + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Email") } + EmailInput(value = emailTextState.value.email, + attrs = { + required() + classes("mdc-text-field__input") + onInput { + viewModel.onEvent(RegisterEvent.EnteredEmail(it.value)) + + } + }) + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Password") } + PasswordInput(value = passwordTextState.value.password, + attrs = { + required() + classes("mdc-text-field__input") + onInput { + viewModel.onEvent(RegisterEvent.EnteredPassword(it.value)) + } + }) + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Repeat Password") } + PasswordInput(value = confirmedPasswordTextState.value.confirmedPassword, + attrs = { + required() + classes("mdc-text-field__input") + onInput { + viewModel.onEvent(RegisterEvent.EnteredConfirmedPassword(it.value)) + } + }) + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + SubmitInput( + attrs = { + classes("mdc-button", "mdc-button--raised") + value("Submit") + }) + } + } + } + if (openSnackbar) { + FeedbackSnackbar("Passwords do not match") { openSnackbar = false } + } +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/settings/SettingsChangeUserDataView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/settings/SettingsChangeUserDataView.kt new file mode 100644 index 00000000..d86916e6 --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/settings/SettingsChangeUserDataView.kt @@ -0,0 +1,213 @@ +package de.hsfl.budgetBinder.screens.settings + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.compose.FeedbackSnackbar +import de.hsfl.budgetBinder.compose.MainFlexContainer +import de.hsfl.budgetBinder.compose.NavBar +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import de.hsfl.budgetBinder.presentation.event.UiEvent +import de.hsfl.budgetBinder.presentation.viewmodel.settings.EditUserEvent +import de.hsfl.budgetBinder.presentation.viewmodel.settings.SettingsEditUserViewModel +import di +import kotlinx.coroutines.flow.collectLatest +import org.jetbrains.compose.web.attributes.InputType +import org.jetbrains.compose.web.attributes.required +import org.jetbrains.compose.web.css.percent +import org.jetbrains.compose.web.css.width +import org.jetbrains.compose.web.dom.* +import org.kodein.di.instance + + +@Composable +fun SettingsChangeUserDataView() { + val viewModel: SettingsEditUserViewModel by di.instance() + val loadingState = remember { mutableStateOf(false) } + val firstNameText = viewModel.firstNameText.collectAsState() + val lastNameText = viewModel.lastNameText.collectAsState() + val passwordText = viewModel.passwordText.collectAsState() + val confirmedPasswordText = viewModel.confirmedPassword.collectAsState() + var openSnackbar by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = true) { + viewModel.eventFlow.collectLatest { event -> + when (event) { + is UiEvent.ShowLoading -> loadingState.value = true + is UiEvent.HideSuccess -> loadingState.value = false + else -> loadingState.value = false + } + } + } + NavBar {} + MainFlexContainer { + H1(attrs = { classes(AppStylesheet.h1) }) { Text("Change User Data") } + Form(attrs = { + this.addEventListener("submit") { + if (!confirmedPasswordText.value.confirmedPasswordIsValid) { + openSnackbar = true + } + viewModel.onEvent(EditUserEvent.OnUpdate) + it.preventDefault() + } + } + ) { + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Firstname") } + Input( + type = InputType.Text + ) { + required() + classes("mdc-text-field__input") + value(firstNameText.value.firstName) + onInput { + viewModel.onEvent(EditUserEvent.EnteredFirstName(it.value)) + } + } + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Lastname") } + Input( + type = InputType.Text + ) { + required() + classes("mdc-text-field__input") + value(lastNameText.value.lastName) + onInput { + viewModel.onEvent(EditUserEvent.EnteredLastName(it.value)) + } + } + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Password") } + PasswordInput(value = passwordText.value.password, + attrs = { + required() + classes("mdc-text-field__input") + onInput { + viewModel.onEvent(EditUserEvent.EnteredPassword(it.value)) + } + }) + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + Label( + attrs = { + classes("mdc-text-field", "mdc-text-field--filled", AppStylesheet.width) + } + ) { + Span( + attrs = { + classes("mdc-text-field__ripple") + } + ) { } + Span( + attrs = { + classes("mdc-floating-label", "mdc-floating-label--float-above") + } + ) { Text("Repeat Password") } + PasswordInput(value = confirmedPasswordText.value.confirmedPassword, + attrs = { + required() + classes("mdc-text-field__input") + onInput { + viewModel.onEvent(EditUserEvent.EnteredConfirmedPassword(it.value)) + } + }) + Span( + attrs = { + classes("mdc-line-ripple") + } + ) { } + } + } + Div( + attrs = { + classes(AppStylesheet.margin) + } + ) { + SubmitInput( + attrs = { + classes("mdc-button", "mdc-button--raised") + value("Submit") + }) + } + } + } + if (openSnackbar) { + FeedbackSnackbar("Passwords do not match") { openSnackbar = false } + } +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/settings/SettingsComponent.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/settings/SettingsComponent.kt new file mode 100644 index 00000000..bfde94f5 --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/settings/SettingsComponent.kt @@ -0,0 +1,35 @@ +package de.hsfl.budgetBinder.screens.settings + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.presentation.Screen +import de.hsfl.budgetBinder.presentation.event.UiEvent +import de.hsfl.budgetBinder.presentation.flow.DataFlow +import de.hsfl.budgetBinder.presentation.flow.RouterFlow +import de.hsfl.budgetBinder.presentation.viewmodel.settings.SettingsViewModel +import di +import kotlinx.coroutines.flow.collectLatest +import org.kodein.di.instance + +@Composable +fun SettingsComponent() { + val viewModel: SettingsViewModel by di.instance() + val dataFlow: DataFlow by di.instance() + val routerFlow: RouterFlow by di.instance() + val userState = dataFlow.userState.collectAsState() + val screenState = routerFlow.state.collectAsState() + val loadingState = remember { mutableStateOf(false) } + + LaunchedEffect(key1 = true) { + viewModel.eventFlow.collectLatest { + when (it) { + is UiEvent.ShowLoading -> loadingState.value = true + else -> loadingState.value = false + } + } + } + when (screenState.value) { + is Screen.Settings.Menu -> SettingsView() + is Screen.Settings.User -> SettingsChangeUserDataView() + else -> {} + } +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/settings/SettingsView.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/settings/SettingsView.kt new file mode 100644 index 00000000..43db4647 --- /dev/null +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/de/hsfl/budgetBinder/screens/settings/SettingsView.kt @@ -0,0 +1,74 @@ +package de.hsfl.budgetBinder.screens.settings + +import androidx.compose.runtime.* +import de.hsfl.budgetBinder.compose.DeleteDialog +import de.hsfl.budgetBinder.compose.MainFlexContainer +import de.hsfl.budgetBinder.compose.NavBar +import de.hsfl.budgetBinder.compose.theme.AppStylesheet +import de.hsfl.budgetBinder.presentation.viewmodel.settings.SettingsEvent +import de.hsfl.budgetBinder.presentation.viewmodel.settings.SettingsViewModel +import di +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.* +import org.kodein.di.instance + + +@Composable +fun SettingsView() { + val viewModel: SettingsViewModel by di.instance() + var deleteDialog by remember { mutableStateOf(false) } + + NavBar { } + MainFlexContainer { + H1(attrs = { classes(AppStylesheet.h1) }) { Text("Settings") } + Div( + attrs = { + classes(AppStylesheet.margin, AppStylesheet.flexContainer) + } + ) { + Button( + attrs = { + classes("mdc-button", "mdc-button--raised", AppStylesheet.flex100) + onClick { viewModel.onEvent(SettingsEvent.OnChangeToSettingsUserEdit) } + } + ) { + Text("Change Userdata") + } + } + Div( + attrs = { + classes(AppStylesheet.margin, AppStylesheet.flexContainer) + } + ) { + Button( + attrs = { + classes("mdc-button", "mdc-button--raised", AppStylesheet.flex100) + onClick { viewModel.onEvent(SettingsEvent.OnLogoutAllDevices) } + } + ) { + Text("Logout on all device") + } + } + Div( + attrs = { + classes(AppStylesheet.margin, AppStylesheet.flexContainer) + } + ) { + Button( + attrs = { + classes("mdc-button", "mdc-button--raised", AppStylesheet.flex100) + onClick { deleteDialog = true } + style { backgroundColor(Color("#b00020")) } + } + ) { + Text("Delete User") + } + } + } + if (deleteDialog) { + DeleteDialog( + false, + { viewModel.onEvent(SettingsEvent.OnDeleteDialogConfirm) }, + { deleteDialog = false }) { Text("Delete User?") } + } +} diff --git a/budget-binder-multiplatform-app/src/jsMain/kotlin/main.kt b/budget-binder-multiplatform-app/src/jsMain/kotlin/main.kt index bdeb7a75..72aa52b4 100644 --- a/budget-binder-multiplatform-app/src/jsMain/kotlin/main.kt +++ b/budget-binder-multiplatform-app/src/jsMain/kotlin/main.kt @@ -1,12 +1,18 @@ import androidx.compose.runtime.* +import de.hsfl.budgetBinder.compose.FeedbackSnackbar import de.hsfl.budgetBinder.compose.Router import de.hsfl.budgetBinder.compose.theme.AppStylesheet import de.hsfl.budgetBinder.di.kodein -import de.hsfl.budgetBinder.presentation.Screen +import de.hsfl.budgetBinder.presentation.event.UiEvent +import de.hsfl.budgetBinder.presentation.flow.UiEventSharedFlow +import de.hsfl.budgetBinder.presentation.viewmodel.navdrawer.NavDrawerEvent import io.ktor.client.engine.js.* -import org.jetbrains.compose.web.css.Style +import kotlinx.coroutines.flow.collectLatest +import org.jetbrains.compose.web.css.* +import org.jetbrains.compose.web.dom.Img import org.jetbrains.compose.web.renderComposable import org.kodein.di.compose.withDI +import org.kodein.di.instance fun main() { renderComposable("root") { @@ -19,6 +25,37 @@ val di = kodein(ktorEngine = Js.create()) @Composable fun App() = withDI(di) { - val screenState = remember { mutableStateOf(Screen.Login) } - Router(screenState = screenState) + val uiEventFlow: UiEventSharedFlow by di.instance() + val loadingState = remember { mutableStateOf(true) } + val snackBarText = remember { mutableStateOf("") } + val snackBarHidden = remember { mutableStateOf(true) } + LaunchedEffect(key1 = true) { + uiEventFlow.eventFlow.collectLatest { event -> + when (event) { + is UiEvent.ShowLoading -> loadingState.value = true + is UiEvent.ShowError -> { + loadingState.value = false + snackBarText.value = event.msg + snackBarHidden.value = false + } + is UiEvent.ShowSuccess -> { + loadingState.value = false + snackBarText.value = event.msg + snackBarHidden.value = false + } + else -> {} + } + } + } + if (!loadingState.value) { // I don't understand why it needs to be inverted to work? + Img( + src = "images/Loading.gif", alt = "Logo", attrs = { + classes(AppStylesheet.loadingImage) + } + ) + } + FeedbackSnackbar(snackBarText.value, snackBarHidden.value) { + snackBarHidden.value = true + } + Router() } diff --git a/budget-binder-multiplatform-app/src/jsMain/resources/cart.png b/budget-binder-multiplatform-app/src/jsMain/resources/cart.png deleted file mode 100644 index 5eaa2661..00000000 Binary files a/budget-binder-multiplatform-app/src/jsMain/resources/cart.png and /dev/null differ diff --git a/budget-binder-multiplatform-app/src/jsMain/resources/images/Loading.gif b/budget-binder-multiplatform-app/src/jsMain/resources/images/Loading.gif new file mode 100644 index 00000000..faadfe5b Binary files /dev/null and b/budget-binder-multiplatform-app/src/jsMain/resources/images/Loading.gif differ diff --git a/budget-binder-multiplatform-app/src/jsMain/resources/material-design-override.css b/budget-binder-multiplatform-app/src/jsMain/resources/material-design-override.css index b1408a0d..fe94bbf8 100644 --- a/budget-binder-multiplatform-app/src/jsMain/resources/material-design-override.css +++ b/budget-binder-multiplatform-app/src/jsMain/resources/material-design-override.css @@ -23,4 +23,5 @@ --mdc-theme-text-hint-on-dark: rgba(255, 255, 255, 0.5); --mdc-theme-text-disabled-on-dark: rgba(255, 255, 255, 0.5); --mdc-theme-text-icon-on-dark: rgba(255, 255, 255, 0.5); + --mdc-switch-selected-track-color: #64b5f6 } diff --git a/budget-binder-multiplatform-app/src/jvmMain/kotlin/de/hsfl/budgetBinder/Root.kt b/budget-binder-multiplatform-app/src/jvmMain/kotlin/de/hsfl/budgetBinder/Root.kt index 0db8eb37..a0f27381 100644 --- a/budget-binder-multiplatform-app/src/jvmMain/kotlin/de/hsfl/budgetBinder/Root.kt +++ b/budget-binder-multiplatform-app/src/jvmMain/kotlin/de/hsfl/budgetBinder/Root.kt @@ -7,7 +7,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import de.hsfl.budgetBinder.common.User import de.hsfl.budgetBinder.di.kodein import de.hsfl.budgetBinder.presentation.event.UiEvent import de.hsfl.budgetBinder.presentation.flow.DataFlow @@ -36,12 +35,19 @@ fun App() = withDI(di) { is UiEvent.ShowLoading -> loadingState.value = true is UiEvent.ShowError -> { loadingState.value = false - scaffoldState.snackbarHostState.showSnackbar(message = event.msg, actionLabel = "Dismiss") + scaffoldState.snackbarHostState.showSnackbar( + message = event.msg, + actionLabel = "Dismiss" + ) } is UiEvent.ShowSuccess -> { loadingState.value = false - scaffoldState.snackbarHostState.showSnackbar(message = event.msg, actionLabel = "Dismiss") + scaffoldState.snackbarHostState.showSnackbar( + message = event.msg, + actionLabel = "Dismiss" + ) } + else -> {} } } } diff --git a/budget-binder-multiplatform-app/src/jvmMain/kotlin/de/hsfl/budgetBinder/screens/category/CategorySummary.kt b/budget-binder-multiplatform-app/src/jvmMain/kotlin/de/hsfl/budgetBinder/screens/category/CategorySummary.kt index fd642ee8..17810cf3 100644 --- a/budget-binder-multiplatform-app/src/jvmMain/kotlin/de/hsfl/budgetBinder/screens/category/CategorySummary.kt +++ b/budget-binder-multiplatform-app/src/jvmMain/kotlin/de/hsfl/budgetBinder/screens/category/CategorySummary.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import de.hsfl.budgetBinder.common.Category -import de.hsfl.budgetBinder.di import de.hsfl.budgetBinder.presentation.CategoryImageToIcon import de.hsfl.budgetBinder.presentation.event.LifecycleEvent import de.hsfl.budgetBinder.presentation.event.UiEvent