├── resources ├── src │ └── commonMain │ │ ├── resources │ │ └── MR │ │ │ ├── data.txt │ │ │ ├── fonts │ │ │ ├── PTSans-Bold.ttf │ │ │ ├── PTSans-Italic.ttf │ │ │ └── PTSans-Regular.ttf │ │ │ └── images │ │ │ ├── arrow_back_ios.svg │ │ │ ├── icon_bookmark_fill.svg │ │ │ ├── icon_chevron_right.svg │ │ │ ├── arrow_back_default.svg │ │ │ ├── icon_adidas.svg │ │ │ ├── icon_bookmark.svg │ │ │ ├── icon_code_fill.svg │ │ │ ├── icon_code.svg │ │ │ ├── icon_plant.svg │ │ │ ├── icon_pin_location.svg │ │ │ ├── icon_search.svg │ │ │ ├── icon_home_fill.svg │ │ │ ├── icon_explore_fill.svg │ │ │ ├── icon_home.svg │ │ │ ├── icon_emoji_error.svg │ │ │ ├── icon_shipping.svg │ │ │ ├── icon_explore.svg │ │ │ ├── icon_cart.svg │ │ │ ├── icon_about_fill.svg │ │ │ └── icon_about.svg │ │ └── kotlin │ │ └── com │ │ └── utsman │ │ └── tokobola │ │ └── resources │ │ └── Typealiases.kt ├── resources.podspec └── build.gradle.kts ├── libraries ├── common │ ├── src │ │ ├── iosMain │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── utsman │ │ │ │ └── tokobola │ │ │ │ └── common │ │ │ │ ├── root │ │ │ │ └── component │ │ │ │ ├── ProductTopBar.ios.kt │ │ │ │ └── NativeView.kt │ │ ├── androidMain │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── utsman │ │ │ │ └── tokobola │ │ │ │ └── common │ │ │ │ ├── root │ │ │ │ └── component │ │ │ │ ├── ProductTopBar.android.kt │ │ │ │ └── NativeView.kt │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── com │ │ │ └── utsman │ │ │ └── tokobola │ │ │ └── common │ │ │ ├── entity │ │ │ ├── Cart.kt │ │ │ ├── Category.kt │ │ │ ├── CartProduct.kt │ │ │ ├── Brand.kt │ │ │ ├── LocationPlace.kt │ │ │ ├── HomeBanner.kt │ │ │ ├── Product.kt │ │ │ └── ThumbnailProduct.kt │ │ │ ├── component │ │ │ ├── NativeView.kt │ │ │ ├── Dimens.kt │ │ │ ├── ProductPullRefreshIndicator.kt │ │ │ ├── Visibility.kt │ │ │ ├── Shimmer.kt │ │ │ ├── ScaffoldPullRefresh.kt │ │ │ ├── MapView.kt │ │ │ ├── TopBar.kt │ │ │ └── ErrorScreen.kt │ │ │ └── theme │ │ │ ├── Colors.kt │ │ │ ├── Typography.kt │ │ │ └── Theme.kt │ └── build.gradle.kts ├── core │ ├── src │ │ ├── androidMain │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── utsman │ │ │ │ └── tokobola │ │ │ │ └── core │ │ │ │ ├── utils │ │ │ │ ├── CoroutineExtensions.kt │ │ │ │ ├── AndroidContextProvider.kt │ │ │ │ └── PlatformUtils.android.kt │ │ │ │ ├── SynchronizObject.kt │ │ │ │ ├── Platform.android.kt │ │ │ │ ├── ViewModel.kt │ │ │ │ ├── RememberViewModel.android.kt │ │ │ │ └── ImageLoader.android.kt │ │ ├── commonMain │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── utsman │ │ │ │ └── tokobola │ │ │ │ └── core │ │ │ │ ├── Platform.kt │ │ │ │ ├── SynchronizObject.kt │ │ │ │ ├── utils │ │ │ │ ├── CoroutineExtensions.kt │ │ │ │ ├── PlatformUtils.kt │ │ │ │ ├── StateComposable.kt │ │ │ │ ├── StateExtensions.kt │ │ │ │ ├── LocationUtils.kt │ │ │ │ └── MainExtensions.kt │ │ │ │ ├── RememberViewModel.kt │ │ │ │ ├── data │ │ │ │ ├── ElvisExtensions.kt │ │ │ │ ├── Paged.kt │ │ │ │ └── LatLon.kt │ │ │ │ ├── ImageLoader.kt │ │ │ │ ├── Greeting.kt │ │ │ │ ├── ViewModel.kt │ │ │ │ ├── State.kt │ │ │ │ ├── SingletonCreator.kt │ │ │ │ └── navigation │ │ │ │ ├── Navigation.kt │ │ │ │ └── ScreenContainer.kt │ │ └── iosMain │ │ │ └── kotlin │ │ │ └── com │ │ │ └── utsman │ │ │ └── tokobola │ │ │ └── core │ │ │ ├── utils │ │ │ ├── CoroutineExtensions.kt │ │ │ └── PlatformUtils.ios.kt │ │ │ ├── Platform.ios.kt │ │ │ ├── SynchronizObject.kt │ │ │ ├── RememberViewModel.ios.kt │ │ │ ├── ViewModel.kt │ │ │ └── ImageLoader.ios.kt │ └── build.gradle.kts ├── api │ ├── src │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── com │ │ │ └── utsman │ │ │ └── tokobola │ │ │ └── api │ │ │ ├── WebDataSource.kt │ │ │ ├── LazyWebApi.kt │ │ │ ├── response │ │ │ ├── CategoryResponse.kt │ │ │ ├── BrandResponse.kt │ │ │ ├── HomeBannerResponse.kt │ │ │ ├── ThumbnailProductResponse.kt │ │ │ ├── ProductResponse.kt │ │ │ ├── MapboxSearchResponse.kt │ │ │ └── MapboxReverseResponse.kt │ │ │ ├── WebEndPoint.kt │ │ │ ├── MapboxWebApi.kt │ │ │ └── ProductWebApi.kt │ └── build.gradle.kts ├── database │ ├── src │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── com │ │ │ └── utsman │ │ │ └── tokobola │ │ │ └── database │ │ │ ├── LazyDatabaseRepository.kt │ │ │ └── data │ │ │ ├── WishlistRealm.kt │ │ │ ├── RecentlyViewedRealm.kt │ │ │ ├── LocationPlaceRealm.kt │ │ │ └── CartProductRealm.kt │ └── build.gradle.kts ├── location │ ├── src │ │ ├── commonMain │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── utsman │ │ │ │ └── tokobola │ │ │ │ └── location │ │ │ │ ├── LocationExtensions.kt │ │ │ │ ├── LocationInstanceProvider.kt │ │ │ │ └── LocationTracker.kt │ │ ├── iosMain │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── utsman │ │ │ │ └── tokobola │ │ │ │ └── location │ │ │ │ └── LocationTracker.ios.kt │ │ └── androidMain │ │ │ └── kotlin │ │ │ └── com │ │ │ └── utsman │ │ │ └── tokobola │ │ │ └── location │ │ │ └── LocationTracker.android.kt │ └── build.gradle.kts └── network │ ├── src │ └── commonMain │ │ └── kotlin │ │ └── com │ │ └── utsman │ │ └── tokobola │ │ └── network │ │ ├── response │ │ ├── BaseResponse.kt │ │ └── BasePagedResponse.kt │ │ ├── ApiReducer.kt │ │ ├── DynamicLookupSerializer.kt │ │ ├── NetworkSources.kt │ │ ├── ClientProvider.kt │ │ ├── AutoPagingAdapter.kt │ │ └── StateTransformation.kt │ └── build.gradle.kts ├── doc ├── img.png ├── ss1.png ├── ss2.png ├── ss3.png ├── ss4.png ├── ss5.png └── ss6.png ├── shared ├── src │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── main.android.kt │ ├── commonMain │ │ └── kotlin │ │ │ ├── tab │ │ │ ├── CustomTab.kt │ │ │ ├── WishlistTab.kt │ │ │ ├── HomeTab.kt │ │ │ ├── ExploreTab.kt │ │ │ └── AboutTab.kt │ │ │ ├── ScreenContainerProvider.kt │ │ │ └── NavigationProvider.kt │ └── iosMain │ │ └── kotlin │ │ └── main.ios.kt └── build.gradle.kts ├── readme_images ├── banner.png ├── run_on_android.png ├── target_device.png ├── edit_run_config.png ├── hello_world_ios.png ├── open_project_view.png ├── text_field_added.png └── android_app_running.png ├── iosApp ├── Configuration │ └── Config.xcconfig ├── iosApp │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── app-icon-1024.png │ │ │ └── Contents.json │ │ └── AccentColor.colorset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── iOSApp.swift │ ├── ContentView.swift │ └── Info.plist └── Podfile ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── androidApp ├── src │ └── androidMain │ │ ├── res │ │ ├── values │ │ │ ├── strings.xml │ │ │ └── ic_launcher_background.xml │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_round.webp │ │ │ └── ic_launcher_foreground.webp │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_round.webp │ │ │ └── ic_launcher_foreground.webp │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_round.webp │ │ │ └── ic_launcher_foreground.webp │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_round.webp │ │ │ └── ic_launcher_foreground.webp │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_round.webp │ │ │ └── ic_launcher_foreground.webp │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ ├── ic_launcher-playstore.png │ │ ├── kotlin │ │ └── com │ │ │ └── utsman │ │ │ └── tokobola │ │ │ ├── MainApplication.kt │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml └── build.gradle.kts ├── cleanup.sh ├── close.sh ├── .gitignore ├── features ├── details │ ├── src │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── com │ │ │ └── utsman │ │ │ └── tokobola │ │ │ └── details │ │ │ ├── ui │ │ │ ├── product │ │ │ │ ├── ProductDetailUiConfig.kt │ │ │ │ └── ProductDetailViewModel.kt │ │ │ ├── brand │ │ │ │ └── BrandDetailViewModel.kt │ │ │ └── category │ │ │ │ └── CategoryDetailViewModel.kt │ │ │ ├── domain │ │ │ ├── BrandDetailUseCase.kt │ │ │ ├── CategoryDetailUseCase.kt │ │ │ ├── DetailRepository.kt │ │ │ └── ProductDetailUseCase.kt │ │ │ └── DetailInstanceProvider.kt │ └── build.gradle.kts ├── wishlist │ ├── src │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── com │ │ │ └── utsman │ │ │ └── tokobola │ │ │ └── wishlist │ │ │ ├── ui │ │ │ ├── WishlistViewModel.kt │ │ │ └── WishlishCompose.kt │ │ │ ├── domain │ │ │ ├── WishlistRepository.kt │ │ │ └── WishlistUseCase.kt │ │ │ └── WishlistInstanceProvider.kt │ └── build.gradle.kts ├── cart │ ├── src │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── com │ │ │ └── utsman │ │ │ └── tokobola │ │ │ └── cart │ │ │ ├── ui │ │ │ ├── CartUiConfig.kt │ │ │ ├── LocationPickerViewModel.kt │ │ │ └── CartViewModel.kt │ │ │ ├── CartInstanceProvider.kt │ │ │ └── domain │ │ │ ├── CartRepository.kt │ │ │ └── LocationPickerUseCase.kt │ └── build.gradle.kts ├── explore │ ├── src │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── com │ │ │ └── utsman │ │ │ └── tokobola │ │ │ └── explore │ │ │ ├── ui │ │ │ ├── ExploreUiConfig.kt │ │ │ ├── search │ │ │ │ └── SearchViewModel.kt │ │ │ └── explore │ │ │ │ └── ExploreViewModel.kt │ │ │ ├── domain │ │ │ ├── ExploreRepository.kt │ │ │ └── search │ │ │ │ └── SearchUseCase.kt │ │ │ └── ExploreInstanceProvider.kt │ └── build.gradle.kts └── home │ ├── src │ └── commonMain │ │ └── kotlin │ │ └── com │ │ └── utsman │ │ └── tokobola │ │ └── home │ │ ├── HomeInstanceProvider.kt │ │ ├── ui │ │ ├── HomeBrand.kt │ │ └── HomeViewModel.kt │ │ └── domain │ │ ├── HomeRepository.kt │ │ └── HomeUseCase.kt │ └── build.gradle.kts ├── cleanup-open.sh ├── gradle.properties ├── README.md ├── settings.gradle.kts └── gradlew.bat /resources/src/commonMain/resources/MR/data.txt: -------------------------------------------------------------------------------- 1 | anu -------------------------------------------------------------------------------- /libraries/common/src/iosMain/kotlin/com/utsman/tokobola/common/root: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/common/src/androidMain/kotlin/com/utsman/tokobola/common/root: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/doc/img.png -------------------------------------------------------------------------------- /doc/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/doc/ss1.png -------------------------------------------------------------------------------- /doc/ss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/doc/ss2.png -------------------------------------------------------------------------------- /doc/ss3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/doc/ss3.png -------------------------------------------------------------------------------- /doc/ss4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/doc/ss4.png -------------------------------------------------------------------------------- /doc/ss5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/doc/ss5.png -------------------------------------------------------------------------------- /doc/ss6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/doc/ss6.png -------------------------------------------------------------------------------- /shared/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /readme_images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/banner.png -------------------------------------------------------------------------------- /iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=com.utsman.tokobola 3 | APP_NAME=TokoBola 4 | -------------------------------------------------------------------------------- /readme_images/run_on_android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/run_on_android.png -------------------------------------------------------------------------------- /readme_images/target_device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/target_device.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /readme_images/edit_run_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/edit_run_config.png -------------------------------------------------------------------------------- /readme_images/hello_world_ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/hello_world_ios.png -------------------------------------------------------------------------------- /readme_images/open_project_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/open_project_view.png -------------------------------------------------------------------------------- /readme_images/text_field_added.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/text_field_added.png -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TokoBola 3 | -------------------------------------------------------------------------------- /readme_images/android_app_running.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/readme_images/android_app_running.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/utils/CoroutineExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.utils 2 | 3 | -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /androidApp/src/androidMain/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/ic_launcher-playstore.png -------------------------------------------------------------------------------- /iosApp/Podfile: -------------------------------------------------------------------------------- 1 | target 'iosApp' do 2 | use_frameworks! 3 | platform :ios, '14.1' 4 | pod 'shared', :path => '../shared' 5 | pod 'MapboxMaps', '10.15.0' 6 | end -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/fonts/PTSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/resources/src/commonMain/resources/MR/fonts/PTSans-Bold.ttf -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/fonts/PTSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/resources/src/commonMain/resources/MR/fonts/PTSans-Italic.ttf -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/fonts/PTSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/resources/src/commonMain/resources/MR/fonts/PTSans-Regular.ttf -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/main.android.kt: -------------------------------------------------------------------------------- 1 | import androidx.activity.compose.BackHandler 2 | import androidx.compose.runtime.Composable 3 | 4 | @Composable fun MainView() = App() -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /resources/src/commonMain/kotlin/com/utsman/tokobola/resources/Typealiases.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.resources 2 | 3 | import dev.icerock.moko.graphics.Color 4 | 5 | val MokoColor = Color -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4285F4 4 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utsmannn/tokobola/HEAD/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | interface Platform { 4 | val name: String 5 | } 6 | 7 | expect fun getPlatform(): Platform -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/arrow_back_ios.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/SynchronizObject.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | expect open class SynchronizObject() 4 | 5 | expect inline fun synchroniz(lock: Any, block: () -> T): T -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/utils/CoroutineExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.utils 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /libraries/common/src/iosMain/kotlin/com/utsman/tokobola/common/component/ProductTopBar.ios.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.component 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | -------------------------------------------------------------------------------- /libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/WebDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.api 2 | 3 | import com.utsman.tokobola.network.NetworkSources 4 | 5 | abstract class WebDataSource : NetworkSources(BuildKonfig.BASE_URL) -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/RememberViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | expect fun rememberViewModel(viewModel: () -> T): T -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/data/ElvisExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.data 2 | 3 | fun Int?.orNol(): Int = this ?: 0 4 | fun Double?.orNol(): Double = this ?: 0.0 5 | fun Boolean?.orFalse(): Boolean = this ?: false -------------------------------------------------------------------------------- /libraries/core/src/iosMain/kotlin/com/utsman/tokobola/core/utils/CoroutineExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.utils 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.IO 6 | -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/Cart.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.entity 2 | 3 | data class Cart( 4 | val product: ThumbnailProduct = ThumbnailProduct(), 5 | var quantity: Int = 0, 6 | var millis: Long 7 | ) -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/ImageLoader.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.seiko.imageloader.ImageLoader 5 | 6 | @Composable 7 | expect fun rememberImageLoader(): ImageLoader -------------------------------------------------------------------------------- /libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/SynchronizObject.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | actual open class SynchronizObject 4 | 5 | actual inline fun synchroniz(lock: Any, block: () -> T): T { 6 | return synchronized(lock, block) 7 | } -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/Greeting.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | class Greeting { 4 | private val platform: Platform = getPlatform() 5 | 6 | fun greet(): String { 7 | return "Hello, ${platform.name}!" 8 | } 9 | } -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_bookmark_fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/data/Paged.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.data 2 | 3 | data class Paged( 4 | var data: List = emptyList(), 5 | var hasNextPage: Boolean = false, 6 | var page: Int = 1, 7 | var perPage: Int = 10 8 | ) -------------------------------------------------------------------------------- /cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -rf .idea 3 | ./gradlew clean 4 | rm -rf .gradle 5 | rm -rf build 6 | rm -rf */build 7 | rm -rf iosApp/iosApp.xcworkspace 8 | rm -rf iosApp/Pods 9 | rm -rf iosApp/iosApp.xcodeproj/project.xcworkspace 10 | rm -rf iosApp/iosApp.xcodeproj/xcuserdata 11 | -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/Category.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.entity 2 | 3 | 4 | data class Category( 5 | val id: Int = 0, 6 | val name: String = "", 7 | val description: String = "", 8 | val image: String = "" 9 | ) -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/CartProduct.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.entity 2 | 3 | data class CartProduct( 4 | val productId: Int = 0, 5 | val quantity: Int = 0 6 | ) { 7 | 8 | fun isEmpty() = quantity == 0 9 | } 10 | -------------------------------------------------------------------------------- /close.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | current_dir=$(pwd) 4 | 5 | osascript -e 'tell application "Android Studio" to if it is running then quit' 6 | osascript -e "tell app \"Terminal\" 7 | do script \"cd $current_dir && ./cleanup-open.sh\" 8 | end tell" 9 | 10 | osascript -e 'tell app "Android Studio"' -------------------------------------------------------------------------------- /iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ZStack { 8 | Color.white.ignoresSafeArea(.all) // status bar color 9 | ContentView() 10 | }.preferredColorScheme(.light) 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/Brand.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.entity 2 | 3 | data class Brand( 4 | val id: Int = 0, 5 | val name: String = "", 6 | val description: String = "", 7 | val image: String = "", 8 | val logo: String = "" 9 | ) -------------------------------------------------------------------------------- /libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/Platform.android.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | class AndroidPlatform : Platform { 4 | override val name: String = "Android ${android.os.Build.VERSION.SDK_INT}" 5 | } 6 | 7 | actual fun getPlatform(): Platform = AndroidPlatform() -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_chevron_right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/ViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | 5 | expect abstract class ViewModel() { 6 | val viewModelScope: CoroutineScope 7 | 8 | protected open fun onCleared() 9 | fun clear() 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | build/ 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | iosApp/Podfile.lock 11 | iosApp/Pods/* 12 | iosApp/iosApp.xcworkspace/* 13 | iosApp/iosApp.xcodeproj/* 14 | !iosApp/iosApp.xcodeproj/project.pbxproj 15 | shared/shared.podspec 16 | -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/NativeView.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.component 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | 6 | @Composable 7 | expect fun NativeView(modifier: Modifier = Modifier, content: @Composable () -> Unit) -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/LocationPlace.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.entity 2 | 3 | import com.utsman.tokobola.core.data.LatLon 4 | 5 | data class LocationPlace( 6 | val name: String = "", 7 | val latLon: LatLon = LatLon(), 8 | val bbox: String = "" 9 | ) 10 | -------------------------------------------------------------------------------- /libraries/database/src/commonMain/kotlin/com/utsman/tokobola/database/LazyDatabaseRepository.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.database 2 | 3 | fun localRepository(): Lazy { 4 | return lazy( 5 | mode = LazyThreadSafetyMode.SYNCHRONIZED 6 | ) { 7 | LocalRepository.providedLocalRepository() 8 | } 9 | } -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/arrow_back_default.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/utils/PlatformUtils.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.unit.Dp 5 | 6 | @Composable 7 | expect fun rememberStatusBarHeightDp(): Dp 8 | 9 | @Composable 10 | expect fun rememberNavigationBarHeightDp(): Dp -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_adidas.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_bookmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_code_fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/core/src/iosMain/kotlin/com/utsman/tokobola/core/Platform.ios.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | import platform.UIKit.UIDevice 4 | 5 | class IOSPlatform: Platform { 6 | override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion 7 | } 8 | 9 | actual fun getPlatform(): Platform = IOSPlatform() -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/tab/CustomTab.kt: -------------------------------------------------------------------------------- 1 | package tab 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.painter.Painter 5 | import cafe.adriel.voyager.navigator.tab.Tab 6 | 7 | internal interface CustomTab : Tab { 8 | 9 | val iconSelected: Painter? 10 | @Composable 11 | get() = options.icon 12 | } -------------------------------------------------------------------------------- /features/details/src/commonMain/kotlin/com/utsman/tokobola/details/ui/product/ProductDetailUiConfig.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.details.ui.product 2 | 3 | import androidx.compose.ui.unit.IntSize 4 | 5 | data class ProductDetailUiConfig( 6 | var selectedImageIndex: Int = 0, 7 | var isShowImageDialog: Boolean = false, 8 | var globalSize: IntSize = IntSize.Zero 9 | ) -------------------------------------------------------------------------------- /libraries/location/src/commonMain/kotlin/com/utsman/tokobola/location/LocationExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.location 2 | 3 | import com.utsman.tokobola.core.data.LatLon 4 | import dev.icerock.moko.geo.LatLng 5 | 6 | fun LatLon.isNear(other: LatLon): Boolean { 7 | return LatLng(latitude, longitude).distanceTo(LatLng(other.latitude, other.longitude)) < 30.0 8 | } -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/HomeBanner.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.entity 2 | 3 | data class HomeBanner( 4 | val id: Int = 0, 5 | val productId: Int = 0, 6 | val colorPrimary: String = "#FFFFFF", 7 | val colorAccent: String = "#000000", 8 | val productImage: String = "", 9 | val description: String = "" 10 | ) -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_plant.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/State.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | sealed class State { 4 | class Idle : State() 5 | class Loading : State() 6 | data class Success(val data: T) : State() 7 | data class Failure(val exception: Throwable) : State() 8 | 9 | override fun toString(): String { 10 | return "${this::class.simpleName}" 11 | } 12 | } -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_pin_location.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/theme/Colors.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import com.utsman.tokobola.core.utils.parseString 5 | 6 | val ColorPrimaryDark = Color.parseString("#616161") 7 | val ColorPrimaryLight = Color.parseString("#607D8B") 8 | 9 | val ColorSecondaryDark = Color.parseString("#616161") 10 | val ColorSecondaryLight = Color.parseString("#607D8B") -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_home_fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/Product.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.entity 2 | 3 | data class Product( 4 | val category: String = "", 5 | val description: String = "", 6 | val id: Int = 0, 7 | val images: List = emptyList(), 8 | val name: String = "", 9 | val price: Double = 0.0, 10 | val isPromoted: Boolean = false, 11 | val brand: ThumbnailProduct.ThumbnailBrand = ThumbnailProduct.ThumbnailBrand() 12 | ) -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_explore_fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/LazyWebApi.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.api 2 | 3 | fun productWebApi(): Lazy { 4 | return lazy( 5 | mode = LazyThreadSafetyMode.SYNCHRONIZED 6 | ) { 7 | ProductWebApi.create { ProductWebApi() } 8 | } 9 | } 10 | 11 | fun mapboxWebApi(): Lazy { 12 | return lazy( 13 | mode = LazyThreadSafetyMode.SYNCHRONIZED 14 | ) { 15 | MapboxWebApi.create { MapboxWebApi() } 16 | } 17 | } -------------------------------------------------------------------------------- /libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/response/CategoryResponse.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.api.response 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class CategoryResponse( 9 | @SerialName("id") 10 | val id: Int?, 11 | @SerialName("name") 12 | val name: String?, 13 | @SerialName("description") 14 | val description: String?, 15 | @SerialName("image") 16 | val image: String? 17 | ) -------------------------------------------------------------------------------- /libraries/location/src/commonMain/kotlin/com/utsman/tokobola/location/LocationInstanceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.location 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | 5 | object LocationInstanceProvider { 6 | 7 | fun providedLocationTrackerProvider(): LocationTrackerProvider { 8 | return LocationTrackerProvider.create { LocationTrackerProvider() } 9 | } 10 | } 11 | 12 | val LocalLocationTrackerProvider = compositionLocalOf { error("Not provider") } -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/Dimens.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.component 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | object Dimens { 6 | 7 | val PaddingGeneral = 12.dp 8 | val PaddingItem = 6.dp 9 | val HeightTopBarSearch = 112.dp 10 | val HeightTopBarSearchWithTitle = 142.dp 11 | val HeightProductItemGrid = 280.dp 12 | val HeightProductItemGridRectangle = 156.dp 13 | val CornerSize = 12.dp 14 | val ShadowElevation = 42.dp 15 | } -------------------------------------------------------------------------------- /libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/ViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | import androidx.lifecycle.ViewModel as LifecycleViewModel 4 | import androidx.lifecycle.viewModelScope as androidViewModelScope 5 | 6 | actual abstract class ViewModel : LifecycleViewModel() { 7 | actual val viewModelScope = androidViewModelScope 8 | 9 | actual override fun onCleared() { 10 | super.onCleared() 11 | } 12 | 13 | actual fun clear() { 14 | onCleared() 15 | } 16 | } -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/utils/AndroidContextProvider.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.utils 2 | 3 | import android.content.Context 4 | import com.utsman.tokobola.core.SingletonCreator 5 | 6 | interface AndroidContextProvider { 7 | 8 | val context: Context 9 | 10 | companion object : SingletonCreator() { 11 | fun getInstance(): AndroidContextProvider { 12 | return synchronized(this) { instance as AndroidContextProvider } 13 | } 14 | 15 | } 16 | } -------------------------------------------------------------------------------- /libraries/database/src/commonMain/kotlin/com/utsman/tokobola/database/data/WishlistRealm.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.database.data 2 | 3 | import com.utsman.tokobola.core.utils.nowMillis 4 | import io.realm.kotlin.types.RealmObject 5 | import io.realm.kotlin.types.annotations.PrimaryKey 6 | import org.mongodb.kbson.BsonObjectId 7 | import org.mongodb.kbson.ObjectId 8 | 9 | class WishlistRealm : RealmObject { 10 | @PrimaryKey 11 | var _id: ObjectId = BsonObjectId() 12 | var productId: Int = 0 13 | var date: Long = nowMillis() 14 | } -------------------------------------------------------------------------------- /libraries/location/src/iosMain/kotlin/com/utsman/tokobola/location/LocationTracker.ios.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.location 2 | 3 | import dev.icerock.moko.geo.LocationTracker 4 | import dev.icerock.moko.permissions.ios.PermissionsController 5 | import platform.CoreLocation.kCLLocationAccuracyBest 6 | 7 | internal actual val locationTracker: LocationTracker 8 | get() { 9 | return LocationTracker( 10 | permissionsController = PermissionsController(), 11 | accuracy = kCLLocationAccuracyBest 12 | ) 13 | } -------------------------------------------------------------------------------- /libraries/database/src/commonMain/kotlin/com/utsman/tokobola/database/data/RecentlyViewedRealm.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.database.data 2 | 3 | import com.utsman.tokobola.core.utils.nowMillis 4 | import io.realm.kotlin.types.RealmObject 5 | import io.realm.kotlin.types.annotations.PrimaryKey 6 | import org.mongodb.kbson.BsonObjectId 7 | import org.mongodb.kbson.ObjectId 8 | 9 | class RecentlyViewedRealm : RealmObject { 10 | @PrimaryKey 11 | var _id: ObjectId = BsonObjectId() 12 | var productId: Int = 0 13 | var date: Long = nowMillis() 14 | } -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_emoji_error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/response/BrandResponse.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.api.response 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class BrandResponse( 9 | @SerialName("id") 10 | val id: Int?, 11 | @SerialName("name") 12 | val name: String?, 13 | @SerialName("description") 14 | val description: String?, 15 | @SerialName("image") 16 | val image: String?, 17 | @SerialName("logo") 18 | val logo: String? 19 | ) -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_shipping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/SingletonCreator.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | import kotlin.jvm.Volatile 4 | 5 | open class SingletonCreator : SynchronizObject() { 6 | 7 | @Volatile 8 | var instance: T? = null 9 | 10 | fun create(creator: () -> T): T { 11 | synchroniz(this) { 12 | if (instance == null) { 13 | instance = creator() 14 | } 15 | return instance as T 16 | } 17 | } 18 | 19 | fun getInstanceOrThrow(): T = checkNotNull(instance) 20 | } -------------------------------------------------------------------------------- /libraries/core/src/iosMain/kotlin/com/utsman/tokobola/core/SynchronizObject.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | import kotlinx.atomicfu.locks.synchronized 4 | import kotlinx.atomicfu.locks.SynchronizedObject as AtomicSynchronizedObject 5 | 6 | actual open class SynchronizObject : AtomicSynchronizedObject() 7 | 8 | actual inline fun synchroniz(lock: Any, block: () -> T): T { 9 | return if (lock is AtomicSynchronizedObject) { 10 | synchronized(lock, block) 11 | } else { 12 | throw IllegalArgumentException("Lock param invalid!") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/response/BaseResponse.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSerializationApi::class) 2 | 3 | package com.utsman.tokobola.network.response 4 | 5 | 6 | import kotlinx.serialization.ExperimentalSerializationApi 7 | import kotlinx.serialization.SerialName 8 | import kotlinx.serialization.Serializable 9 | 10 | @Serializable 11 | data class BaseResponse( 12 | @SerialName("data") 13 | var `data`: T?, 14 | @SerialName("message") 15 | var message: String?, 16 | @SerialName("status") 17 | var status: Boolean? 18 | ) -------------------------------------------------------------------------------- /libraries/common/src/iosMain/kotlin/com/utsman/tokobola/common/component/NativeView.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.component 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.interop.UIKitView 6 | import androidx.compose.ui.window.ComposeUIViewController 7 | 8 | @Composable 9 | actual fun NativeView(modifier: Modifier, content: @Composable () -> Unit) { 10 | UIKitView( 11 | factory = { 12 | ComposeUIViewController(content).view() 13 | }, 14 | modifier = modifier 15 | ) 16 | } -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_explore.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/database/src/commonMain/kotlin/com/utsman/tokobola/database/data/LocationPlaceRealm.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.database.data 2 | 3 | import io.realm.kotlin.types.RealmObject 4 | import io.realm.kotlin.types.annotations.PrimaryKey 5 | import org.mongodb.kbson.BsonObjectId 6 | import org.mongodb.kbson.ObjectId 7 | 8 | class LocationPlaceRealm : RealmObject { 9 | @PrimaryKey 10 | var _id: ObjectId = BsonObjectId() 11 | var key: String = "" 12 | var name: String = "" 13 | var latitude: Double = 0.0 14 | var longitude: Double = 0.0 15 | var bbox: String = "" 16 | } -------------------------------------------------------------------------------- /libraries/location/src/androidMain/kotlin/com/utsman/tokobola/location/LocationTracker.android.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.location 2 | 3 | import com.utsman.tokobola.core.utils.AndroidContextProvider 4 | import dev.icerock.moko.geo.LocationTracker 5 | import dev.icerock.moko.permissions.PermissionsController 6 | 7 | internal actual val locationTracker: LocationTracker 8 | get() { 9 | val contextProvider = AndroidContextProvider.getInstance() 10 | return LocationTracker( 11 | PermissionsController(applicationContext = contextProvider.context) 12 | ) 13 | } -------------------------------------------------------------------------------- /libraries/core/src/iosMain/kotlin/com/utsman/tokobola/core/RememberViewModel.ios.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ComposeNodeLifecycleCallback 5 | import androidx.compose.runtime.DisposableEffect 6 | import androidx.compose.runtime.remember 7 | 8 | @Composable 9 | actual fun rememberViewModel(viewModel: () -> T): T { 10 | val vm = remember { viewModel.invoke() } 11 | DisposableEffect(vm) { 12 | onDispose { 13 | vm.clear() 14 | } 15 | } 16 | 17 | return vm 18 | } -------------------------------------------------------------------------------- /features/wishlist/src/commonMain/kotlin/com/utsman/tokobola/wishlist/ui/WishlistViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.wishlist.ui 2 | 3 | import com.utsman.tokobola.core.ViewModel 4 | import com.utsman.tokobola.wishlist.domain.WishlistUseCase 5 | import kotlinx.coroutines.flow.asStateFlow 6 | import kotlinx.coroutines.launch 7 | 8 | class WishlistViewModel(private val useCase: WishlistUseCase) : ViewModel() { 9 | 10 | val productWishlistState get() = useCase.productWishlistReducer.dataFlow.asStateFlow() 11 | 12 | fun listenProductWishlist() = viewModelScope.launch { 13 | useCase.getWishlist() 14 | } 15 | } -------------------------------------------------------------------------------- /iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import shared 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | Main_iosKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(edges: .bottom) // Compose has own keyboard handler 17 | .ignoresSafeArea() 18 | } 19 | } 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /androidApp/src/androidMain/kotlin/com/utsman/tokobola/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import com.utsman.tokobola.core.utils.AndroidContextProvider 6 | 7 | class MainApplication : Application() { 8 | 9 | override fun onCreate() { 10 | super.onCreate() 11 | 12 | val androidContextProvider = object : AndroidContextProvider { 13 | override val context: Context 14 | get() = this@MainApplication 15 | } 16 | 17 | AndroidContextProvider.create { androidContextProvider } 18 | } 19 | } -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/navigation/Navigation.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.navigation 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import com.utsman.tokobola.core.data.LatLon 5 | 6 | interface Navigation { 7 | fun back(): Boolean 8 | 9 | fun goToDetailProduct(id: Int): Boolean 10 | fun goToDetailCategory(categoryId: Int): Boolean 11 | fun goToDetailBrand(brandId: Int): Boolean 12 | fun goToSearch(): Boolean 13 | fun goToCart(): Boolean 14 | fun goToLocationPicker(latLon: LatLon): Boolean 15 | } 16 | 17 | val LocalNavigation = compositionLocalOf { error("navigation failure") } -------------------------------------------------------------------------------- /libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/RememberViewModel.android.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.ui.platform.LocalLifecycleOwner 7 | 8 | @Composable 9 | actual fun rememberViewModel(viewModel: () -> T): T { 10 | val lifecycle = LocalLifecycleOwner.current.lifecycle 11 | val vm = remember { viewModel.invoke() } 12 | DisposableEffect(lifecycle) { 13 | onDispose { 14 | vm.clear() 15 | } 16 | } 17 | 18 | return vm 19 | } -------------------------------------------------------------------------------- /libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/response/HomeBannerResponse.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.api.response 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class HomeBannerResponse( 9 | @SerialName("product_id") 10 | val productId: Int?, 11 | @SerialName("color_primary") 12 | val colorPrimary: String?, 13 | @SerialName("color_accent") 14 | val colorAccent: String?, 15 | @SerialName("id") 16 | val id: Int?, 17 | @SerialName("product_image") 18 | val productImage: String?, 19 | @SerialName("description") 20 | val description: String? 21 | ) -------------------------------------------------------------------------------- /libraries/common/src/androidMain/kotlin/com/utsman/tokobola/common/component/ProductTopBar.android.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.component 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.rounded.ArrowBack 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.ColorFilter 13 | import androidx.compose.ui.unit.dp 14 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/main.ios.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.layout.fillMaxSize 2 | import androidx.compose.runtime.Composable 3 | import androidx.compose.runtime.DisposableEffect 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.interop.UIKitView 7 | import androidx.compose.ui.window.ComposeUIViewController 8 | import kotlinx.cinterop.useContents 9 | import platform.CoreLocation.CLLocationCoordinate2DMake 10 | import platform.MapKit.MKCoordinateRegionMakeWithDistance 11 | import platform.MapKit.MKMapView 12 | import platform.MapKit.MKPointAnnotation 13 | 14 | fun MainViewController() = ComposeUIViewController { App() } -------------------------------------------------------------------------------- /features/wishlist/src/commonMain/kotlin/com/utsman/tokobola/wishlist/domain/WishlistRepository.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.wishlist.domain 2 | 3 | import com.utsman.tokobola.api.productWebApi 4 | import com.utsman.tokobola.core.SingletonCreator 5 | import com.utsman.tokobola.database.localRepository 6 | 7 | class WishlistRepository { 8 | 9 | private val productWebApi by productWebApi() 10 | private val localRepository by localRepository() 11 | 12 | suspend fun getThumbnailByIds(ids: List) = productWebApi.getThumbnailByIds(ids) 13 | 14 | suspend fun getAllWishlist() = localRepository.selectAllWishlist() 15 | 16 | companion object : SingletonCreator() 17 | } -------------------------------------------------------------------------------- /features/cart/src/commonMain/kotlin/com/utsman/tokobola/cart/ui/CartUiConfig.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.cart.ui 2 | 3 | import com.utsman.tokobola.common.entity.Cart 4 | import com.utsman.tokobola.core.utils.nowMillis 5 | 6 | data class CartUiConfig( 7 | var carts: List = emptyList(), 8 | val time: Long = nowMillis() 9 | ) { 10 | fun amount(): Double { 11 | return carts.sumOf { it.product.price * it.quantity } 12 | } 13 | 14 | fun isCartEmpty(): Boolean { 15 | return amount() <= 0.0 16 | } 17 | 18 | companion object { 19 | const val KEY_LOCATION_CURRENT = "current_location" 20 | const val KEY_LOCATION_SHIPPING = "shipping_location" 21 | } 22 | } -------------------------------------------------------------------------------- /libraries/database/src/commonMain/kotlin/com/utsman/tokobola/database/data/CartProductRealm.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.database.data 2 | 3 | import com.utsman.tokobola.core.utils.nowMillis 4 | import io.realm.kotlin.types.RealmObject 5 | import io.realm.kotlin.types.annotations.PrimaryKey 6 | import org.mongodb.kbson.BsonObjectId 7 | import org.mongodb.kbson.ObjectId 8 | 9 | class CartProductRealm : RealmObject { 10 | @PrimaryKey 11 | var _id: ObjectId = BsonObjectId() 12 | var productId: Int = 0 13 | var quantity: Int = 0 14 | 15 | var millis = nowMillis() 16 | 17 | override fun toString(): String { 18 | return "[_id: $_id, productId: $productId, quantity: $quantity]" 19 | } 20 | } -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/entity/ThumbnailProduct.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.entity 2 | 3 | data class ThumbnailProduct( 4 | val id: Int = 0, 5 | val name: String = "", 6 | val price: Double = 0.0, 7 | val category: ThumbnailCategory = ThumbnailCategory(), 8 | val brand: ThumbnailBrand = ThumbnailBrand(), 9 | val image: String = "", 10 | val promoted: Boolean = false 11 | ) { 12 | data class ThumbnailBrand( 13 | val id: Int = 0, 14 | val name: String = "", 15 | val logo: String = "" 16 | ) 17 | 18 | data class ThumbnailCategory( 19 | val id: Int = 0, 20 | val name: String = "", 21 | ) 22 | 23 | } -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/utils/StateComposable.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.utsman.tokobola.core.State 5 | 6 | @Composable 7 | fun State.onLoadingComposed(content: @Composable () -> Unit) { 8 | if (this is State.Loading) { content.invoke() } 9 | } 10 | 11 | @Composable 12 | fun State.onSuccessComposed(content: @Composable (T) -> Unit) { 13 | if (this is State.Success) { content.invoke(this.data) } 14 | } 15 | 16 | @Composable 17 | fun State.onFailureComposed(content: @Composable (Throwable) -> Unit) { 18 | if (this is State.Failure) { content.invoke(this.exception) } 19 | } -------------------------------------------------------------------------------- /features/explore/src/commonMain/kotlin/com/utsman/tokobola/explore/ui/ExploreUiConfig.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.explore.ui 2 | 3 | import com.utsman.tokobola.common.entity.Brand 4 | import com.utsman.tokobola.common.entity.Category 5 | import com.utsman.tokobola.common.entity.ThumbnailProduct 6 | import com.utsman.tokobola.core.State 7 | 8 | data class ExploreUiConfig( 9 | val offsetTabCategory: Float = 0f, 10 | val heightTabCategory: Int = 0, 11 | val selectedTabCategoryIndex: Int = 0, 12 | val selectedTabBrandIndex: Int = 0, 13 | val selectedCategory: Category = Category(id = 1), 14 | val selectedBrand: Brand = Brand(id = 1), 15 | val selectedProductCategory: State> = State.Loading() 16 | ) -------------------------------------------------------------------------------- /cleanup-open.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -rf .idea 3 | ./gradlew clean 4 | rm -rf .gradle 5 | rm -rf build 6 | rm -rf */build 7 | rm -rf iosApp/iosApp.xcworkspace 8 | rm -rf iosApp/Pods 9 | rm -rf iosApp/iosApp.xcodeproj/project.xcworkspace 10 | rm -rf iosApp/iosApp.xcodeproj/xcuserdata 11 | 12 | echo -en "> Reopen Android Studio when not running on 5 second.." 13 | sleep 2 14 | echo -en "\r> Reopen Android Studio when not running on 4 second.." 15 | sleep 2 16 | echo -en "\r> Reopen Android Studio when not running on 3 second.." 17 | sleep 2 18 | echo -en "\r> Reopen Android Studio when not running on 2 second.." 19 | sleep 2 20 | echo -en "\r> Reopen Android Studio when not running on 1 second.." 21 | sleep 2 22 | 23 | open -a /Applications/Android\ Studio.app -------------------------------------------------------------------------------- /libraries/common/src/androidMain/kotlin/com/utsman/tokobola/common/component/NativeView.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.component 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.platform.ComposeView 6 | import androidx.compose.ui.platform.LocalContext 7 | import androidx.compose.ui.viewinterop.AndroidView 8 | 9 | @Composable 10 | actual fun NativeView(modifier: Modifier, content: @Composable () -> Unit) { 11 | val context = LocalContext.current 12 | AndroidView( 13 | factory = { 14 | ComposeView(context).apply { 15 | setContent { content.invoke() } 16 | } 17 | }, 18 | modifier = modifier 19 | ) 20 | } -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/data/LatLon.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.data 2 | 3 | import kotlinx.serialization.json.Json 4 | import kotlinx.serialization.json.decodeFromJsonElement 5 | 6 | data class LatLon( 7 | val latitude: Double = 0.0, 8 | val longitude: Double = 0.0 9 | ) { 10 | fun json(): String { 11 | return "{\"latitude\": \"$latitude\", \"longitude\":\"$longitude\"}" 12 | } 13 | 14 | fun isBlank(): Boolean { 15 | return latitude == 0.0 16 | } 17 | 18 | companion object { 19 | fun fromJson(json: String): LatLon { 20 | val element = Json.parseToJsonElement(json) 21 | return Json.decodeFromJsonElement(element) 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /libraries/core/src/iosMain/kotlin/com/utsman/tokobola/core/ViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.IO 6 | import kotlinx.coroutines.SupervisorJob 7 | import kotlinx.coroutines.cancel 8 | import kotlin.coroutines.CoroutineContext 9 | 10 | actual abstract class ViewModel { 11 | actual val viewModelScope: CoroutineScope = object : CoroutineScope { 12 | override val coroutineContext: CoroutineContext 13 | get() = SupervisorJob() + Dispatchers.IO 14 | } 15 | 16 | protected actual open fun onCleared() { 17 | viewModelScope.cancel() 18 | } 19 | 20 | actual fun clear() { 21 | onCleared() 22 | } 23 | } -------------------------------------------------------------------------------- /features/home/src/commonMain/kotlin/com/utsman/tokobola/home/HomeInstanceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.home 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import com.utsman.tokobola.core.SynchronizObject 5 | import com.utsman.tokobola.core.synchroniz 6 | import com.utsman.tokobola.home.domain.HomeRepository 7 | import com.utsman.tokobola.home.domain.HomeUseCase 8 | import kotlin.jvm.Volatile 9 | import kotlin.native.concurrent.ThreadLocal 10 | 11 | object HomeInstanceProvider { 12 | 13 | fun providedUseCase(): HomeUseCase { 14 | val repo = HomeRepository.create { HomeRepository() } 15 | return HomeUseCase.create { HomeUseCase(repo) } 16 | } 17 | } 18 | 19 | val LocalHomeUseCase = compositionLocalOf { error("Not Provided") } -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_cart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_about_fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" 3 | 4 | #Kotlin 5 | kotlin.code.style=official 6 | 7 | #MPP 8 | kotlin.mpp.stability.nowarn=true 9 | kotlin.mpp.enableCInteropCommonization=true 10 | kotlin.mpp.androidSourceSetLayoutVersion=2 11 | 12 | #Compose 13 | org.jetbrains.compose.experimental.uikit.enabled=true 14 | kotlin.native.cacheKind=none 15 | 16 | #Android 17 | android.useAndroidX=true 18 | android.compileSdk=33 19 | android.targetSdk=33 20 | android.minSdk=24 21 | 22 | #Versions 23 | kotlin.version=1.8.20 24 | agp.version=7.4.2 25 | compose.version=1.4.1 26 | 27 | #public properties 28 | base.url=https://footballstore.fly.dev/api 29 | mapbox.base.url=https://api.mapbox.com 30 | mapbox.token=sk.eyJ1Ijoia3VjaW5nYXBlcyIsImEiOiJjbGxmdnVtbGIwemdqM2txaHZtam5teGxiIn0.4AlEjvqBWJgaYOElmi5bxA -------------------------------------------------------------------------------- /libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/response/BasePagedResponse.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.network.response 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class BasePagedResponse( 8 | @SerialName("data") 9 | var `data`: DataResponse?, 10 | @SerialName("message") 11 | var message: String?, 12 | @SerialName("status") 13 | var status: Boolean? 14 | ) { 15 | @Serializable 16 | data class DataResponse( 17 | @SerialName("data") 18 | var `data`: List = emptyList(), 19 | @SerialName("has_next_page") 20 | var hasNextPage: Boolean = false, 21 | @SerialName("page") 22 | var page: Int = 1, 23 | @SerialName("per_page") 24 | var perPage: Int = 10 25 | ) 26 | } -------------------------------------------------------------------------------- /resources/src/commonMain/resources/MR/images/icon_about.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/ProductPullRefreshIndicator.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.component 2 | 3 | import androidx.compose.foundation.layout.offset 4 | import androidx.compose.material.ExperimentalMaterialApi 5 | import androidx.compose.material.pullrefresh.PullRefreshIndicator 6 | import androidx.compose.material.pullrefresh.PullRefreshState 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import com.utsman.tokobola.core.utils.rememberStatusBarHeightDp 10 | 11 | @OptIn(ExperimentalMaterialApi::class) 12 | @Composable 13 | fun PullRefreshIndicatorOffset( 14 | refreshing: Boolean, 15 | state: PullRefreshState, 16 | modifier: Modifier = Modifier 17 | ) { 18 | val offset = rememberStatusBarHeightDp() 19 | PullRefreshIndicator( 20 | refreshing, state, modifier.offset(y = offset) 21 | ) 22 | } -------------------------------------------------------------------------------- /features/wishlist/src/commonMain/kotlin/com/utsman/tokobola/wishlist/WishlistInstanceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.wishlist 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import com.utsman.tokobola.core.SynchronizObject 5 | import com.utsman.tokobola.core.synchroniz 6 | import com.utsman.tokobola.wishlist.domain.WishlistRepository 7 | import com.utsman.tokobola.wishlist.domain.WishlistUseCase 8 | import kotlin.jvm.Volatile 9 | import kotlin.native.concurrent.ThreadLocal 10 | 11 | object WishlistInstanceProvider { 12 | private fun getRepository(): WishlistRepository { 13 | return WishlistRepository.create { WishlistRepository() } 14 | } 15 | 16 | fun providedUseCase(): WishlistUseCase { 17 | return WishlistUseCase.create { WishlistUseCase(getRepository()) } 18 | } 19 | } 20 | 21 | val LocalWishlistUseCase = compositionLocalOf { error("Not provided") } -------------------------------------------------------------------------------- /androidApp/src/androidMain/kotlin/com/utsman/tokobola/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola 2 | 3 | import MainView 4 | import android.os.Bundle 5 | import androidx.activity.compose.setContent 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.core.view.WindowCompat 9 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 10 | 11 | class MainActivity : AppCompatActivity() { 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | val currentWindow = window 15 | WindowCompat.setDecorFitsSystemWindows(currentWindow, false) 16 | 17 | setContent { 18 | val systemUiController = rememberSystemUiController() 19 | systemUiController.setStatusBarColor(Color.Transparent, darkIcons = true) 20 | MainView() 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/ApiReducer.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.network 2 | 3 | import com.utsman.tokobola.core.State 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | 6 | open class ApiReducer { 7 | 8 | val dataFlow: MutableStateFlow> = MutableStateFlow(State.Idle()) 9 | 10 | suspend inline fun transform( 11 | transformation: StateTransformation = StateTransformation.DefaultResponseTransform(), 12 | noinline call: suspend () -> U, 13 | noinline mapper: (U) -> T 14 | ) { 15 | dataFlow.value = State.Loading() 16 | val result = transformation.transform(call, mapper) 17 | dataFlow.value = result 18 | } 19 | 20 | fun forcePushState(state: State) { 21 | dataFlow.value = state 22 | } 23 | 24 | fun clear() { 25 | dataFlow.value = State.Idle() 26 | } 27 | } -------------------------------------------------------------------------------- /libraries/core/src/iosMain/kotlin/com/utsman/tokobola/core/utils/PlatformUtils.ios.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.unit.Dp 6 | import androidx.compose.ui.unit.dp 7 | import kotlinx.cinterop.useContents 8 | import platform.UIKit.UIApplication 9 | 10 | @Composable 11 | actual fun rememberStatusBarHeightDp(): Dp { 12 | return remember { 13 | val currentHeight = UIApplication.sharedApplication.statusBarFrame.useContents { 14 | this.size.height 15 | } 16 | val statusBarHeight = if (currentHeight > 0) { 17 | currentHeight.toInt() 18 | } else { 19 | 0 20 | } 21 | statusBarHeight 22 | }.dp 23 | } 24 | 25 | 26 | @Composable 27 | actual fun rememberNavigationBarHeightDp(): Dp { 28 | return remember { 29 | 30 30 | }.dp 31 | } -------------------------------------------------------------------------------- /features/explore/src/commonMain/kotlin/com/utsman/tokobola/explore/domain/ExploreRepository.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.explore.domain 2 | 3 | import com.utsman.tokobola.api.productWebApi 4 | import com.utsman.tokobola.core.SingletonCreator 5 | 6 | class ExploreRepository { 7 | private val productApi by productWebApi() 8 | 9 | suspend fun getBrand() = productApi.getBrand() 10 | suspend fun getCategory() = productApi.getCategory() 11 | suspend fun getProductBrand(brandId: Int, page: Int) = productApi.getByBrandPaged(brandId, page) 12 | suspend fun getProductCategory(categoryId: Int, page: Int) = productApi.getByCategoryPaged(categoryId, page) 13 | suspend fun getProductTop() = productApi.getTop() 14 | suspend fun getProductCurated() = productApi.getCurated() 15 | 16 | suspend fun getProductBySearch(page: Int, query: String) = productApi.getBySearch(page, query) 17 | 18 | companion object : SingletonCreator() 19 | } -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/Visibility.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.component 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.AnimatedVisibilityScope 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.animation.fadeIn 7 | import androidx.compose.animation.fadeOut 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | 11 | @Composable 12 | fun DefaultAnimatedVisibility( 13 | isVisible: Boolean, 14 | modifier: Modifier = Modifier, 15 | duration: Int = 200, 16 | content: @Composable AnimatedVisibilityScope.() -> Unit 17 | ) { 18 | AnimatedVisibility( 19 | visible = isVisible, 20 | enter = fadeIn(animationSpec = tween(duration)), 21 | exit = fadeOut(animationSpec = tween(duration)), 22 | modifier = modifier, 23 | content = content 24 | ) 25 | } -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/Shimmer.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.component 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.RectangleShape 12 | import androidx.compose.ui.unit.Dp 13 | import androidx.compose.ui.unit.dp 14 | 15 | @Composable 16 | fun Shimmer(modifier: Modifier = Modifier.height(320.dp)) { 17 | Box( 18 | modifier = modifier 19 | .fillMaxWidth() 20 | .padding(6.dp) 21 | .shimmerBackground(shape = RoundedCornerShape(Dimens.CornerSize)) 22 | ) 23 | } -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/utils/StateExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.utils 2 | 3 | import com.utsman.tokobola.core.State 4 | 5 | fun State.onLoading(content: () -> Unit) { 6 | if (this is State.Loading) { 7 | content.invoke() 8 | } 9 | } 10 | 11 | fun State.onSuccess(content: (T) -> Unit) { 12 | if (this is State.Success) { 13 | content.invoke(this.data) 14 | } 15 | } 16 | 17 | fun State.onFailure(content: (Throwable) -> Unit) { 18 | if (this is State.Failure) { 19 | content.invoke(this.exception) 20 | } 21 | } 22 | 23 | fun State.onIdle(content: () -> Unit) { 24 | if (this is State.Idle) { 25 | content.invoke() 26 | } 27 | } 28 | 29 | fun State.getOrNull(): T? { 30 | return when (this) { 31 | is State.Success -> { 32 | data 33 | } 34 | 35 | else -> { 36 | null 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/theme/Typography.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.text.font.FontFamily 6 | import com.utsman.tokobola.resources.SharedRes 7 | import dev.icerock.moko.resources.compose.asFont 8 | 9 | @Composable 10 | fun Type(): Typography { 11 | val regularRes = SharedRes.fonts.PTSans.regular 12 | val italicRes = SharedRes.fonts.PTSans.italic 13 | val boldRes = SharedRes.fonts.PTSans.bold 14 | 15 | val regularFont = regularRes.asFont() 16 | val italicFont = italicRes.asFont() 17 | val boldFont = boldRes.asFont() 18 | 19 | val defaultFontFamily = if (regularFont != null && italicFont != null && boldFont != null) { 20 | FontFamily(regularFont, italicFont, boldFont) 21 | } else { 22 | FontFamily.Default 23 | } 24 | 25 | return Typography( 26 | defaultFontFamily = defaultFontFamily 27 | ) 28 | } -------------------------------------------------------------------------------- /features/details/src/commonMain/kotlin/com/utsman/tokobola/details/ui/brand/BrandDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.details.ui.brand 2 | 3 | import com.utsman.tokobola.common.entity.ThumbnailProduct 4 | import com.utsman.tokobola.core.ViewModel 5 | import com.utsman.tokobola.details.domain.BrandDetailUseCase 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.launch 8 | 9 | class BrandDetailViewModel(private val useCase: BrandDetailUseCase) : ViewModel() { 10 | 11 | val productListState get() = useCase.productPagedReducer.dataFlow 12 | val productListFlow = MutableStateFlow(emptyList()) 13 | val brandTitle = MutableStateFlow("") 14 | 15 | fun getProduct(brandId: Int) = viewModelScope.launch { 16 | useCase.getProduct(brandId) 17 | } 18 | 19 | fun pushProductList(productList: List) = viewModelScope.launch { 20 | productListFlow.value = productList 21 | brandTitle.value = productList.firstOrNull()?.brand?.name.orEmpty() 22 | } 23 | 24 | fun clearData() { 25 | useCase.clear() 26 | } 27 | } -------------------------------------------------------------------------------- /libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/utils/PlatformUtils.android.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.utils 2 | import androidx.compose.foundation.layout.WindowInsets 3 | import androidx.compose.foundation.layout.asPaddingValues 4 | import androidx.compose.foundation.layout.navigationBars 5 | import androidx.compose.foundation.layout.statusBars 6 | import androidx.compose.foundation.layout.systemBars 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.platform.LocalContext 10 | import androidx.compose.ui.unit.Density 11 | import androidx.compose.ui.unit.Dp 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | actual fun rememberStatusBarHeightDp(): Dp { 16 | val statusBarPadding = WindowInsets.statusBars.asPaddingValues() 17 | return statusBarPadding.calculateTopPadding().value.toInt().dp 18 | } 19 | 20 | @Composable 21 | actual fun rememberNavigationBarHeightDp(): Dp { 22 | val navBarPadding = WindowInsets.navigationBars.asPaddingValues() 23 | return navBarPadding.calculateBottomPadding().value.toInt().dp 24 | } -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/utils/LocationUtils.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.utils 2 | 3 | import com.utsman.tokobola.core.data.LatLon 4 | import kotlin.math.cos 5 | 6 | fun calculateBboxMapbox(center: LatLon, distanceKm: Double): String { 7 | val latRadian = center.latitude.radian() 8 | val lonRadian = center.longitude.radian() 9 | val latOffset = distanceKm / EARTH_RADIUS 10 | 11 | // Calculate latitude range 12 | val minLat =(latRadian - latOffset).degrees() 13 | val maxLat = (latRadian + latOffset).degrees() 14 | 15 | // Calculate longitude range 16 | val lonOffset = distanceKm / (EARTH_RADIUS * cos(latRadian)) 17 | val minLon = (lonRadian - lonOffset).degrees() 18 | val maxLon = (lonRadian + lonOffset).degrees() 19 | 20 | return "$minLon,$minLat,$maxLon,$maxLat" 21 | } 22 | 23 | private fun Double.radian(): Double { 24 | return this * PI / 180.0 25 | } 26 | 27 | private fun Double.degrees(): Double { 28 | return this * 180.0 / PI 29 | } 30 | 31 | private const val PI = 3.14159265358979323846 32 | private const val EARTH_RADIUS = 6371.0 33 | -------------------------------------------------------------------------------- /features/details/src/commonMain/kotlin/com/utsman/tokobola/details/ui/category/CategoryDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.details.ui.category 2 | 3 | import com.utsman.tokobola.common.entity.ThumbnailProduct 4 | import com.utsman.tokobola.core.ViewModel 5 | import com.utsman.tokobola.details.domain.CategoryDetailUseCase 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.launch 8 | 9 | class CategoryDetailViewModel(private val useCase: CategoryDetailUseCase) : ViewModel() { 10 | 11 | val productListState get() = useCase.productPagedReducer.dataFlow 12 | val productListFlow = MutableStateFlow(emptyList()) 13 | val categoryTitle = MutableStateFlow("") 14 | 15 | fun getProduct(categoryId: Int) = viewModelScope.launch { 16 | useCase.getProduct(categoryId) 17 | } 18 | 19 | fun pushProductList(productList: List) = viewModelScope.launch { 20 | productListFlow.value = productList 21 | categoryTitle.value = productList.firstOrNull()?.category?.name.orEmpty() 22 | } 23 | 24 | fun clearData() { 25 | useCase.clear() 26 | } 27 | } -------------------------------------------------------------------------------- /features/cart/src/commonMain/kotlin/com/utsman/tokobola/cart/CartInstanceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.cart 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import com.utsman.tokobola.cart.domain.CartRepository 5 | import com.utsman.tokobola.cart.domain.CartUseCase 6 | import com.utsman.tokobola.cart.domain.LocationPickerUseCase 7 | import com.utsman.tokobola.location.LocationTrackerProvider 8 | 9 | object CartInstanceProvider { 10 | 11 | private fun getRepository(): CartRepository { 12 | return CartRepository.create { CartRepository() } 13 | } 14 | 15 | fun providedCartUseCase(locationTrackerProvider: LocationTrackerProvider): CartUseCase { 16 | return CartUseCase.create { CartUseCase(getRepository(), locationTrackerProvider) } 17 | } 18 | 19 | fun providedLocationPickerUseCase(): LocationPickerUseCase { 20 | return LocationPickerUseCase.create { LocationPickerUseCase(getRepository()) } 21 | } 22 | } 23 | 24 | val LocalCartUseCase = compositionLocalOf { error("Not provided") } 25 | val LocalLocationPickerUseCase = compositionLocalOf { error("Not provided") } -------------------------------------------------------------------------------- /features/details/src/commonMain/kotlin/com/utsman/tokobola/details/domain/BrandDetailUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.details.domain 2 | 3 | import com.utsman.tokobola.common.entity.ThumbnailProduct 4 | import com.utsman.tokobola.common.toThumbnailProduct 5 | import com.utsman.tokobola.core.SingletonCreator 6 | import com.utsman.tokobola.core.data.Paged 7 | import com.utsman.tokobola.network.ApiReducer 8 | import com.utsman.tokobola.network.AutoPagingAdapter 9 | 10 | class BrandDetailUseCase(private val detailRepository: DetailRepository) { 11 | 12 | val productPagedReducer = ApiReducer>() 13 | private val productPagedAdapter = AutoPagingAdapter(productPagedReducer) 14 | 15 | suspend fun getProduct(brandId: Int) { 16 | productPagedAdapter.executeResponse( 17 | call = { page -> 18 | detailRepository.getProductBrand(brandId, page) 19 | }, 20 | mapper = { 21 | it.toThumbnailProduct() 22 | } 23 | ) 24 | } 25 | 26 | fun clear() = productPagedAdapter.clear() 27 | 28 | companion object : SingletonCreator() 29 | } -------------------------------------------------------------------------------- /libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/response/ThumbnailProductResponse.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.api.response 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ThumbnailProductResponse( 8 | @SerialName("id") 9 | val id: Int?, 10 | @SerialName("name") 11 | val name: String?, 12 | @SerialName("price") 13 | val price: Double?, 14 | @SerialName("brand") 15 | val brand: BrandResponse?, 16 | @SerialName("image") 17 | val image: String?, 18 | @SerialName("category") 19 | val category: CategoryResponse?, 20 | @SerialName("promoted") 21 | val promoted: Boolean? 22 | ) { 23 | @Serializable 24 | data class BrandResponse( 25 | @SerialName("id") 26 | val id: Int?, 27 | @SerialName("name") 28 | val name: String?, 29 | @SerialName("logo") 30 | val logo: String? 31 | ) 32 | 33 | @Serializable 34 | data class CategoryResponse( 35 | @SerialName("id") 36 | val id: Int?, 37 | @SerialName("name") 38 | val name: String? 39 | ) 40 | 41 | } -------------------------------------------------------------------------------- /features/details/src/commonMain/kotlin/com/utsman/tokobola/details/domain/CategoryDetailUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.details.domain 2 | 3 | import com.utsman.tokobola.common.entity.ThumbnailProduct 4 | import com.utsman.tokobola.common.toThumbnailProduct 5 | import com.utsman.tokobola.core.SingletonCreator 6 | import com.utsman.tokobola.core.data.Paged 7 | import com.utsman.tokobola.network.ApiReducer 8 | import com.utsman.tokobola.network.AutoPagingAdapter 9 | 10 | class CategoryDetailUseCase(private val detailRepository: DetailRepository) { 11 | 12 | val productPagedReducer = ApiReducer>() 13 | private val productPagedAdapter = AutoPagingAdapter(productPagedReducer) 14 | 15 | suspend fun getProduct(categoryId: Int) { 16 | productPagedAdapter.executeResponse( 17 | call = { page -> 18 | detailRepository.getProductCategory(categoryId, page) 19 | }, 20 | mapper = { 21 | it.toThumbnailProduct() 22 | } 23 | ) 24 | } 25 | 26 | fun clear() = productPagedAdapter.clear() 27 | 28 | companion object : SingletonCreator() 29 | } -------------------------------------------------------------------------------- /libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/WebEndPoint.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.api 2 | 3 | object WebEndPoint { 4 | 5 | const val PRODUCT_FEATURED = "/v2/product/featured?page={page}" 6 | const val PRODUCT_BRAND = "/v2/product/brand/{brand_id}?page={page}" 7 | const val PRODUCT_CATEGORY = "/v2/product/category/{category_id}?page={page}" 8 | const val PRODUCT_SEARCH = "/v2/product/search?q={query}&page={page}" 9 | const val PRODUCT_THUMBNAIL = "/v2/product/thumbnail?id={id}" 10 | const val PRODUCT_TOP = "/v2/product/top" 11 | const val PRODUCT_CURATED = "/v2/product/curated" 12 | const val PRODUCT_DETAIL = "/v2/product/{product_id}" 13 | const val BANNER = "/v2/product/banner" 14 | const val BRAND = "/brand" 15 | const val CATEGORY = "/category" 16 | 17 | const val MAPBOX_GEOCODING = "/geocoding/v5/mapbox.places/{lon},{lat}.json?access_token={mapbox_access_token}" 18 | const val MAPBOX_SEARCH = "/geocoding/v5/mapbox.places/{q}.json?access_token={mapbox_access_token}&proximity={proximity}" 19 | } 20 | 21 | internal fun String.withParam(key: String, value: Any): String { 22 | return replace("{$key}", "$value") 23 | } -------------------------------------------------------------------------------- /features/home/src/commonMain/kotlin/com/utsman/tokobola/home/ui/HomeBrand.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.home.ui 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material.MaterialTheme 13 | import androidx.compose.material.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.clip 18 | import androidx.compose.ui.text.style.TextAlign 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.sp 21 | import com.seiko.imageloader.rememberImagePainter 22 | import com.utsman.tokobola.common.component.Dimens 23 | import com.utsman.tokobola.common.component.shimmerBackground 24 | import com.utsman.tokobola.common.entity.Brand 25 | 26 | -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/navigation/ScreenContainer.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.compositionLocalOf 5 | import cafe.adriel.voyager.core.screen.Screen 6 | import cafe.adriel.voyager.core.screen.ScreenKey 7 | import com.utsman.tokobola.core.data.LatLon 8 | 9 | interface ScreenContainer { 10 | 11 | fun home(): Screen 12 | fun detailProduct(productId: Int): Screen 13 | fun detailCategory(categoryId: Int): Screen 14 | fun detailBrand(brandId: Int): Screen 15 | fun explore(): Screen 16 | fun wishlist(): Screen 17 | fun search(): Screen 18 | fun cart(): Screen 19 | fun locationPicker(latLon: LatLon): Screen 20 | 21 | } 22 | 23 | val LocalScreenContainer = compositionLocalOf { error("screen container not found") } 24 | 25 | fun screenContentOf(key: String? = null, content: @Composable () -> Unit): Screen { 26 | return object : Screen { 27 | override val key: ScreenKey = key ?: super.key 28 | 29 | @Composable 30 | override fun Content() { 31 | content.invoke() 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /libraries/database/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("com.android.library") 4 | id("org.jetbrains.compose") 5 | kotlin("plugin.serialization") version "1.8.21" 6 | id("io.realm.kotlin") version "1.10.0" 7 | } 8 | 9 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) 10 | kotlin { 11 | targetHierarchy.default() 12 | 13 | android { 14 | compilations.all { 15 | kotlinOptions { 16 | jvmTarget = "1.8" 17 | } 18 | } 19 | } 20 | 21 | listOf( 22 | iosX64(), 23 | iosArm64(), 24 | iosSimulatorArm64() 25 | ).forEach { 26 | it.binaries.framework { 27 | baseName = "database" 28 | } 29 | } 30 | 31 | sourceSets { 32 | val commonMain by getting { 33 | dependencies { 34 | implementation(project(":libraries:core")) 35 | api("io.realm.kotlin:library-base:1.10.0") 36 | } 37 | } 38 | } 39 | } 40 | 41 | android { 42 | namespace = "com.utsman.tokobola.database" 43 | compileSdk = 33 44 | defaultConfig { 45 | minSdk = 24 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/DynamicLookupSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.network 2 | 3 | import kotlinx.serialization.ContextualSerializer 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlinx.serialization.InternalSerializationApi 6 | import kotlinx.serialization.KSerializer 7 | import kotlinx.serialization.descriptors.SerialDescriptor 8 | import kotlinx.serialization.encoding.Decoder 9 | import kotlinx.serialization.encoding.Encoder 10 | import kotlinx.serialization.serializer 11 | 12 | @ExperimentalSerializationApi 13 | class DynamicLookupSerializer: KSerializer { 14 | override val descriptor: SerialDescriptor = ContextualSerializer(Any::class, null, emptyArray()).descriptor 15 | 16 | @OptIn(InternalSerializationApi::class) 17 | override fun serialize(encoder: Encoder, value: Any) { 18 | val actualSerializer = encoder.serializersModule.getContextual(value::class) ?: value::class.serializer() 19 | encoder.encodeSerializableValue(actualSerializer as KSerializer, value) 20 | } 21 | 22 | override fun deserialize(decoder: Decoder): Any { 23 | return decoder.decodeSerializableValue(this) 24 | } 25 | } -------------------------------------------------------------------------------- /libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/response/ProductResponse.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.api.response 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class ProductResponse( 8 | @SerialName("brand") 9 | var brand: BrandResponse?, 10 | @SerialName("category") 11 | var category: CategoryResponse?, 12 | @SerialName("description") 13 | var description: String?, 14 | @SerialName("id") 15 | var id: Int?, 16 | @SerialName("images") 17 | var images: List?, 18 | @SerialName("name") 19 | var name: String?, 20 | @SerialName("price") 21 | var price: Double?, 22 | @SerialName("promoted") 23 | var promoted: Boolean? 24 | ) { 25 | @Serializable 26 | data class BrandResponse( 27 | @SerialName("id") 28 | var id: Int?, 29 | @SerialName("name") 30 | var name: String?, 31 | @SerialName("logo") 32 | var logo: String? 33 | ) 34 | 35 | @Serializable 36 | data class CategoryResponse( 37 | @SerialName("id") 38 | var id: Int?, 39 | @SerialName("name") 40 | var name: String? 41 | ) 42 | } -------------------------------------------------------------------------------- /features/explore/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("com.android.library") 4 | id("org.jetbrains.compose") 5 | kotlin("plugin.serialization") version "1.8.21" 6 | } 7 | 8 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) 9 | kotlin { 10 | targetHierarchy.default() 11 | 12 | android { 13 | compilations.all { 14 | kotlinOptions { 15 | jvmTarget = "1.8" 16 | } 17 | } 18 | } 19 | 20 | listOf( 21 | iosX64(), 22 | iosArm64(), 23 | iosSimulatorArm64() 24 | ).forEach { 25 | it.binaries.framework { 26 | baseName = "explore" 27 | } 28 | } 29 | 30 | sourceSets { 31 | val commonMain by getting { 32 | dependencies { 33 | implementation(project(":libraries:core")) 34 | implementation(project(":libraries:network")) 35 | 36 | implementation(project(":libraries:common")) 37 | } 38 | } 39 | } 40 | } 41 | 42 | android { 43 | namespace = "com.utsman.tokobola.explore" 44 | compileSdk = 33 45 | defaultConfig { 46 | minSdk = 24 47 | } 48 | } -------------------------------------------------------------------------------- /features/explore/src/commonMain/kotlin/com/utsman/tokobola/explore/ExploreInstanceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.explore 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import com.utsman.tokobola.core.SynchronizObject 5 | import com.utsman.tokobola.core.synchroniz 6 | import com.utsman.tokobola.explore.domain.ExploreRepository 7 | import com.utsman.tokobola.explore.domain.explore.ExploreUseCase 8 | import com.utsman.tokobola.explore.domain.search.SearchUseCase 9 | import kotlin.jvm.Volatile 10 | import kotlin.native.concurrent.ThreadLocal 11 | 12 | object ExploreInstanceProvider { 13 | 14 | private fun getRepository(): ExploreRepository { 15 | return ExploreRepository.create { ExploreRepository() } 16 | } 17 | 18 | fun providedExploreUseCase(): ExploreUseCase { 19 | return ExploreUseCase.create { ExploreUseCase(getRepository()) } 20 | } 21 | 22 | fun providedSearchUseCase(): SearchUseCase { 23 | return SearchUseCase.create { SearchUseCase(getRepository()) } 24 | } 25 | } 26 | 27 | val LocalExploreUseCase = compositionLocalOf { error("Explore UseCase not provided") } 28 | val LocalSearchUseCase = compositionLocalOf { error("Search UseCase not provided") } -------------------------------------------------------------------------------- /features/home/src/commonMain/kotlin/com/utsman/tokobola/home/domain/HomeRepository.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.home.domain 2 | 3 | import com.utsman.tokobola.api.productWebApi 4 | import com.utsman.tokobola.core.SingletonCreator 5 | import com.utsman.tokobola.database.data.RecentlyViewedRealm 6 | import com.utsman.tokobola.database.localRepository 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | class HomeRepository { 10 | 11 | private val productApi by productWebApi() 12 | private val localRepository by localRepository() 13 | 14 | suspend fun getFeaturedProductPaged(page: Int) = productApi.getByFeaturedPaged(page) 15 | suspend fun getBanner() = productApi.getHomeBanner() 16 | suspend fun getBrand() = productApi.getBrand() 17 | 18 | suspend fun getThumbnailByIds(ids: List) = productApi.getThumbnailByIds(ids) 19 | 20 | suspend fun markAsViewed(productId: Int) { 21 | localRepository.insertRecentlyViewed(RecentlyViewedRealm().apply { 22 | this.productId = productId 23 | }) 24 | } 25 | 26 | suspend fun getAllRecentlyViewedFlow(): Flow> { 27 | return localRepository.selectAllRecentlyViewed() 28 | } 29 | 30 | companion object : SingletonCreator() 31 | } -------------------------------------------------------------------------------- /features/home/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("com.android.library") 4 | id("org.jetbrains.compose") 5 | kotlin("plugin.serialization") version "1.8.21" 6 | } 7 | 8 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) 9 | kotlin { 10 | targetHierarchy.default() 11 | 12 | android { 13 | compilations.all { 14 | kotlinOptions { 15 | jvmTarget = "1.8" 16 | } 17 | } 18 | } 19 | 20 | listOf( 21 | iosX64(), 22 | iosArm64(), 23 | iosSimulatorArm64() 24 | ).forEach { 25 | it.binaries.framework { 26 | baseName = "home" 27 | } 28 | } 29 | 30 | sourceSets { 31 | val commonMain by getting { 32 | dependencies { 33 | implementation(project(":libraries:core")) 34 | implementation(project(":libraries:network")) 35 | implementation(project(":libraries:database")) 36 | 37 | implementation(project(":libraries:common")) 38 | } 39 | } 40 | } 41 | } 42 | 43 | android { 44 | namespace = "com.utsman.tokobola.home" 45 | compileSdk = 33 46 | defaultConfig { 47 | minSdk = 24 48 | } 49 | } -------------------------------------------------------------------------------- /features/details/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("com.android.library") 4 | id("org.jetbrains.compose") 5 | kotlin("plugin.serialization") version "1.8.21" 6 | } 7 | 8 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) 9 | kotlin { 10 | targetHierarchy.default() 11 | 12 | android { 13 | compilations.all { 14 | kotlinOptions { 15 | jvmTarget = "1.8" 16 | } 17 | } 18 | } 19 | 20 | listOf( 21 | iosX64(), 22 | iosArm64(), 23 | iosSimulatorArm64() 24 | ).forEach { 25 | it.binaries.framework { 26 | baseName = "details" 27 | } 28 | } 29 | 30 | sourceSets { 31 | val commonMain by getting { 32 | dependencies { 33 | implementation(project(":libraries:core")) 34 | implementation(project(":libraries:network")) 35 | implementation(project(":libraries:database")) 36 | 37 | implementation(project(":libraries:common")) 38 | } 39 | } 40 | } 41 | } 42 | 43 | android { 44 | namespace = "com.utsman.tokobola.details" 45 | compileSdk = 33 46 | defaultConfig { 47 | minSdk = 24 48 | } 49 | } -------------------------------------------------------------------------------- /features/wishlist/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("com.android.library") 4 | id("org.jetbrains.compose") 5 | kotlin("plugin.serialization") version "1.8.21" 6 | } 7 | 8 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) 9 | kotlin { 10 | targetHierarchy.default() 11 | 12 | android { 13 | compilations.all { 14 | kotlinOptions { 15 | jvmTarget = "1.8" 16 | } 17 | } 18 | } 19 | 20 | listOf( 21 | iosX64(), 22 | iosArm64(), 23 | iosSimulatorArm64() 24 | ).forEach { 25 | it.binaries.framework { 26 | baseName = "wishlist" 27 | } 28 | } 29 | 30 | sourceSets { 31 | val commonMain by getting { 32 | dependencies { 33 | implementation(project(":libraries:core")) 34 | implementation(project(":libraries:network")) 35 | implementation(project(":libraries:database")) 36 | 37 | implementation(project(":libraries:common")) 38 | } 39 | } 40 | } 41 | } 42 | 43 | android { 44 | namespace = "com.utsman.tokobola.wishlist" 45 | compileSdk = 33 46 | defaultConfig { 47 | minSdk = 24 48 | } 49 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/tab/WishlistTab.kt: -------------------------------------------------------------------------------- 1 | package tab 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.graphics.painter.Painter 6 | import cafe.adriel.voyager.navigator.tab.TabOptions 7 | import com.utsman.tokobola.core.navigation.LocalScreenContainer 8 | import com.utsman.tokobola.resources.SharedRes 9 | import dev.icerock.moko.resources.compose.painterResource 10 | 11 | internal object WishlistTab : CustomTab { 12 | @Composable 13 | override fun Content() { 14 | val screenContainer = LocalScreenContainer.current 15 | screenContainer.wishlist().Content() 16 | } 17 | 18 | override val options: TabOptions 19 | @Composable 20 | get() { 21 | val title = "Wishlist" 22 | val painter = painterResource(SharedRes.images.icon_bookmark) 23 | return remember { 24 | TabOptions( 25 | index = 2u, 26 | title = title, 27 | icon = painter 28 | ) 29 | } 30 | } 31 | 32 | override val iconSelected: Painter? 33 | @Composable 34 | get() { 35 | return painterResource(SharedRes.images.icon_bookmark_fill) 36 | } 37 | } -------------------------------------------------------------------------------- /androidApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("com.android.application") 4 | id("org.jetbrains.compose") 5 | } 6 | 7 | kotlin { 8 | android() 9 | sourceSets { 10 | val androidMain by getting { 11 | dependencies { 12 | implementation("com.google.accompanist:accompanist-systemuicontroller:0.30.1") 13 | implementation(compose.ui) 14 | 15 | implementation(project(":shared")) 16 | implementation(project(":libraries:core")) 17 | } 18 | } 19 | } 20 | } 21 | 22 | android { 23 | compileSdk = (findProperty("android.compileSdk") as String).toInt() 24 | namespace = "com.utsman.tokobola" 25 | 26 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 27 | 28 | defaultConfig { 29 | applicationId = "com.utsman.tokobola" 30 | minSdk = (findProperty("android.minSdk") as String).toInt() 31 | targetSdk = (findProperty("android.targetSdk") as String).toInt() 32 | versionCode = 1 33 | versionName = "1.0" 34 | } 35 | compileOptions { 36 | sourceCompatibility = JavaVersion.VERSION_11 37 | targetCompatibility = JavaVersion.VERSION_11 38 | } 39 | kotlin { 40 | jvmToolchain(11) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/tab/HomeTab.kt: -------------------------------------------------------------------------------- 1 | package tab 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.graphics.painter.Painter 6 | import cafe.adriel.voyager.navigator.tab.Tab 7 | import cafe.adriel.voyager.navigator.tab.TabOptions 8 | import com.utsman.tokobola.core.navigation.LocalScreenContainer 9 | import com.utsman.tokobola.resources.SharedRes 10 | import dev.icerock.moko.resources.compose.painterResource 11 | 12 | internal object HomeTab : CustomTab { 13 | 14 | @Composable 15 | override fun Content() { 16 | val screenContainer = LocalScreenContainer.current 17 | screenContainer.home().Content() 18 | } 19 | 20 | override val options: TabOptions 21 | @Composable 22 | get() { 23 | val title = "Home" 24 | val painter = painterResource(SharedRes.images.icon_home) 25 | 26 | return remember { 27 | TabOptions( 28 | index = 0u, 29 | title = title, 30 | icon = painter 31 | ) 32 | } 33 | } 34 | 35 | override val iconSelected: Painter? 36 | @Composable 37 | get() { 38 | return painterResource(SharedRes.images.icon_home_fill) 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /features/explore/src/commonMain/kotlin/com/utsman/tokobola/explore/ui/search/SearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.explore.ui.search 2 | 3 | import com.utsman.tokobola.common.entity.ThumbnailProduct 4 | import com.utsman.tokobola.core.ViewModel 5 | import com.utsman.tokobola.explore.domain.search.SearchUseCase 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.collectLatest 8 | import kotlinx.coroutines.flow.debounce 9 | import kotlinx.coroutines.flow.distinctUntilChanged 10 | import kotlinx.coroutines.launch 11 | 12 | class SearchViewModel(private val useCase: SearchUseCase) : ViewModel() { 13 | 14 | val query get() = useCase.query 15 | 16 | val productSearchState get() = useCase.productSearchReducer.dataFlow 17 | val productSearchFlow = MutableStateFlow(emptyList()) 18 | 19 | fun postResultSearch(list: List) { 20 | productSearchFlow.value = list 21 | } 22 | 23 | fun listenQuery() = viewModelScope.launch { 24 | query.debounce(2000) 25 | .distinctUntilChanged() 26 | .collectLatest { 27 | useCase.getBySearch(it) 28 | } 29 | } 30 | 31 | fun getNextSearch() = viewModelScope.launch { 32 | useCase.getBySearch(query.value) 33 | } 34 | 35 | fun clearData() { 36 | useCase.clearData() 37 | } 38 | } -------------------------------------------------------------------------------- /features/wishlist/src/commonMain/kotlin/com/utsman/tokobola/wishlist/domain/WishlistUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.wishlist.domain 2 | 3 | import com.utsman.tokobola.common.entity.ThumbnailProduct 4 | import com.utsman.tokobola.common.toThumbnailProduct 5 | import com.utsman.tokobola.core.SingletonCreator 6 | import com.utsman.tokobola.network.ApiReducer 7 | import com.utsman.tokobola.network.StateTransformation 8 | 9 | class WishlistUseCase(private val repository: WishlistRepository) { 10 | 11 | val productWishlistReducer = ApiReducer>() 12 | 13 | suspend fun getWishlist() { 14 | repository.getAllWishlist() 15 | .collect { realms -> 16 | productWishlistReducer.transform( 17 | transformation = StateTransformation.SimpleTransform(), 18 | call = { 19 | val ids = realms.map { it.productId } 20 | repository.getThumbnailByIds(ids) 21 | }, 22 | mapper = { thumbnailProductResponse -> 23 | thumbnailProductResponse.data 24 | ?.map { it.toThumbnailProduct() } 25 | .orEmpty() 26 | } 27 | ) 28 | } 29 | } 30 | 31 | companion object : SingletonCreator() 32 | } -------------------------------------------------------------------------------- /androidApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /libraries/core/src/iosMain/kotlin/com/utsman/tokobola/core/ImageLoader.ios.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.seiko.imageloader.ImageLoader 6 | import com.seiko.imageloader.component.ComponentRegistryBuilder 7 | import com.seiko.imageloader.component.setupDefaultComponents 8 | import okio.Path 9 | import okio.Path.Companion.toPath 10 | import platform.Foundation.NSCachesDirectory 11 | import platform.Foundation.NSSearchPathForDirectoriesInDomains 12 | import platform.Foundation.NSUserDomainMask 13 | 14 | @Composable 15 | actual fun rememberImageLoader(): ImageLoader { 16 | return remember { 17 | val memCacheSize = 32 * 1024 * 1024 18 | val diskCacheSize = 512 * 1024 * 1024 19 | 20 | val cacheDir = NSSearchPathForDirectoriesInDomains( 21 | NSCachesDirectory, NSUserDomainMask, true 22 | ).first().toString() 23 | 24 | val cachePath = ("$cacheDir/media").toPath() 25 | 26 | ImageLoader { 27 | interceptor { 28 | memoryCacheConfig { maxSizeBytes(memCacheSize) } 29 | diskCacheConfig { 30 | directory(cachePath) 31 | maxSizeBytes(diskCacheSize.toLong()) 32 | } 33 | } 34 | 35 | components { setupDefaultComponents() } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/MapboxWebApi.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.api 2 | 3 | import com.utsman.tokobola.api.response.MapboxReverseResponse 4 | import com.utsman.tokobola.api.response.MapboxSearchResponse 5 | import com.utsman.tokobola.core.SingletonCreator 6 | import com.utsman.tokobola.core.data.LatLon 7 | import com.utsman.tokobola.core.utils.calculateBboxMapbox 8 | import com.utsman.tokobola.network.NetworkSources 9 | 10 | class MapboxWebApi : NetworkSources(BuildKonfig.MAPBOX_BASE_URL) { 11 | 12 | 13 | suspend fun getReverseGeocoding(latLon: LatLon, mapboxAccessToken: String): MapboxReverseResponse { 14 | return getRaw( 15 | endPoint = WebEndPoint.MAPBOX_GEOCODING 16 | .withParam("lat", latLon.latitude) 17 | .withParam("lon", latLon.longitude) 18 | .withParam("mapbox_access_token", mapboxAccessToken) 19 | ) 20 | } 21 | 22 | suspend fun getSearchGeocoding(query: String, latLon: LatLon, mapboxAccessToken: String): MapboxSearchResponse { 23 | return getRaw( 24 | endPoint = WebEndPoint.MAPBOX_SEARCH 25 | .withParam("proximity", "${latLon.longitude},${latLon.latitude}") 26 | .withParam("q", query) 27 | .withParam("mapbox_access_token", mapboxAccessToken) 28 | ) 29 | } 30 | 31 | companion object : SingletonCreator() 32 | } -------------------------------------------------------------------------------- /features/details/src/commonMain/kotlin/com/utsman/tokobola/details/DetailInstanceProvider.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.details 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import com.utsman.tokobola.details.domain.BrandDetailUseCase 5 | import com.utsman.tokobola.details.domain.CategoryDetailUseCase 6 | import com.utsman.tokobola.details.domain.DetailRepository 7 | import com.utsman.tokobola.details.domain.ProductDetailUseCase 8 | 9 | object DetailInstanceProvider { 10 | 11 | private fun getRepository(): DetailRepository { 12 | return DetailRepository.create { DetailRepository() } 13 | } 14 | 15 | fun providedProductDetailUseCase(): ProductDetailUseCase { 16 | return ProductDetailUseCase.create { ProductDetailUseCase(getRepository()) } 17 | } 18 | 19 | fun providedCategoryDetailUseCase(): CategoryDetailUseCase { 20 | return CategoryDetailUseCase.create { CategoryDetailUseCase(getRepository()) } 21 | } 22 | 23 | fun providedBrandDetailUseCase(): BrandDetailUseCase { 24 | return BrandDetailUseCase.create { BrandDetailUseCase(getRepository()) } 25 | } 26 | } 27 | 28 | val LocalProductDetailUseCase = compositionLocalOf { error("Not provided") } 29 | val LocalCategoryDetailUseCase = compositionLocalOf { error("Not provided") } 30 | val LocalBrandDetailUseCase = compositionLocalOf { error("Not provided") } -------------------------------------------------------------------------------- /libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/NetworkSources.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.network 2 | 3 | import com.utsman.tokobola.network.response.BasePagedResponse 4 | import com.utsman.tokobola.network.response.BaseResponse 5 | import io.ktor.client.call.body 6 | import io.ktor.client.request.get 7 | import io.ktor.http.ContentType 8 | import io.ktor.http.contentType 9 | 10 | abstract class NetworkSources(protected val baseUrl: String) { 11 | 12 | protected fun client() = ClientProvider.client() 13 | 14 | protected suspend inline fun getRaw( 15 | endPoint: String, 16 | contentType: ContentType = ContentType.Application.Json 17 | ): T { 18 | return client().get("$baseUrl$endPoint") { 19 | contentType(contentType) 20 | }.body() 21 | } 22 | 23 | protected suspend inline fun get( 24 | endPoint: String, 25 | contentType: ContentType = ContentType.Application.Json 26 | ): BaseResponse { 27 | return client().get("$baseUrl$endPoint") { 28 | contentType(contentType) 29 | }.body() 30 | } 31 | 32 | protected suspend inline fun getPaged( 33 | endPoint: String, 34 | contentType: ContentType = ContentType.Application.Json 35 | ): BasePagedResponse { 36 | return client().get("$baseUrl$endPoint") { 37 | contentType(contentType) 38 | }.body() 39 | } 40 | } -------------------------------------------------------------------------------- /libraries/core/src/androidMain/kotlin/com/utsman/tokobola/core/ImageLoader.android.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.core 2 | 3 | import android.content.Context 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.ui.platform.LocalContext 7 | import com.seiko.imageloader.ImageLoader 8 | import com.seiko.imageloader.component.ComponentRegistryBuilder 9 | import com.seiko.imageloader.component.setupDefaultComponents 10 | import com.utsman.tokobola.core.utils.AndroidContextProvider 11 | import okio.Path 12 | import okio.Path.Companion.toPath 13 | 14 | private fun getImageCacheDirectoryPath(context: Context): Path { 15 | return context.cacheDir.absolutePath.toPath() 16 | } 17 | 18 | @Composable 19 | actual fun rememberImageLoader(): ImageLoader { 20 | return remember { 21 | val context = AndroidContextProvider.getInstance().context 22 | val memCacheSize = 32 * 1024 * 1024 23 | val diskCacheSize = 512 * 1024 * 1024 24 | 25 | ImageLoader { 26 | interceptor { 27 | memoryCacheConfig { maxSizeBytes(memCacheSize) } 28 | diskCacheConfig { 29 | directory(getImageCacheDirectoryPath(context)) 30 | maxSizeBytes(diskCacheSize.toLong()) 31 | } 32 | } 33 | 34 | components { 35 | setupDefaultComponents(context) 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/ClientProvider.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.network 2 | 3 | import com.utsman.tokobola.core.SynchronizObject 4 | import com.utsman.tokobola.core.synchroniz 5 | import io.ktor.client.HttpClient 6 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation 7 | import io.ktor.client.plugins.logging.LogLevel 8 | import io.ktor.client.plugins.logging.Logger 9 | import io.ktor.client.plugins.logging.Logging 10 | import io.ktor.client.plugins.logging.SIMPLE 11 | import io.ktor.serialization.kotlinx.json.json 12 | import kotlinx.serialization.json.Json 13 | import kotlin.native.concurrent.ThreadLocal 14 | 15 | 16 | @ThreadLocal 17 | internal object ClientProvider : SynchronizObject() { 18 | 19 | private var _client: HttpClient? = null 20 | 21 | fun client(): HttpClient { 22 | if (_client == null) { 23 | _client = HttpClient { 24 | install(ContentNegotiation) { 25 | json(Json { 26 | prettyPrint = true 27 | isLenient = true 28 | ignoreUnknownKeys = true 29 | explicitNulls = false 30 | }) 31 | } 32 | install(Logging) { 33 | logger = Logger.SIMPLE 34 | level = LogLevel.INFO 35 | } 36 | } 37 | } 38 | return synchroniz(this) { _client!! } 39 | } 40 | } -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.graphics.Color 9 | import com.utsman.tokobola.core.utils.parseString 10 | 11 | private val DarkColorScheme = darkColors( 12 | primary = ColorPrimaryDark, 13 | secondary = ColorSecondaryDark, 14 | secondaryVariant = Color.White 15 | ) 16 | 17 | private val LightColorScheme = lightColors( 18 | primary = ColorPrimaryLight, 19 | secondary = ColorSecondaryLight, 20 | secondaryVariant = Color.White 21 | 22 | /* Other default colors to override 23 | background = Color(0xFFFFFBFE), 24 | surface = Color(0xFFFFFBFE), 25 | onPrimary = Color.White, 26 | onSecondary = Color.White, 27 | onTertiary = Color.White, 28 | onBackground = Color(0xFF1C1B1F), 29 | onSurface = Color(0xFF1C1B1F), 30 | */ 31 | ) 32 | 33 | @Composable 34 | fun CommonTheme( 35 | darkTheme: Boolean = isSystemInDarkTheme(), 36 | content: @Composable () -> Unit 37 | ) { 38 | val colorSchema = if (darkTheme) { 39 | DarkColorScheme 40 | } else { 41 | LightColorScheme 42 | } 43 | 44 | MaterialTheme( 45 | colors = colorSchema, 46 | typography = Type(), 47 | content = content 48 | ) 49 | } -------------------------------------------------------------------------------- /libraries/location/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("com.android.library") 4 | id("org.jetbrains.compose") 5 | kotlin("plugin.serialization") version "1.8.21" 6 | } 7 | 8 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) 9 | kotlin { 10 | targetHierarchy.default() 11 | 12 | android { 13 | compilations.all { 14 | kotlinOptions { 15 | jvmTarget = "1.8" 16 | } 17 | } 18 | } 19 | 20 | listOf( 21 | iosX64(), 22 | iosArm64(), 23 | iosSimulatorArm64() 24 | ).forEach { 25 | it.binaries.framework { 26 | baseName = "location" 27 | } 28 | } 29 | 30 | sourceSets { 31 | val commonMain by getting { 32 | dependencies { 33 | implementation(project(":libraries:core")) 34 | implementation(project(":libraries:network")) 35 | implementation(project(":libraries:database")) 36 | 37 | api("dev.icerock.moko:permissions-compose:0.16.0") 38 | api("dev.icerock.moko:geo-compose:0.6.0") 39 | } 40 | } 41 | 42 | getByName("androidMain") { 43 | dependencies { 44 | api("com.google.android.gms:play-services-location:21.0.1") 45 | } 46 | } 47 | } 48 | } 49 | 50 | android { 51 | namespace = "com.utsman.tokobola.location" 52 | compileSdk = 33 53 | defaultConfig { 54 | minSdk = 24 55 | } 56 | } -------------------------------------------------------------------------------- /libraries/api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | id("com.android.library") 6 | kotlin("plugin.serialization") version "1.8.21" 7 | id("com.codingfeline.buildkonfig") 8 | } 9 | 10 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) 11 | kotlin { 12 | targetHierarchy.default() 13 | 14 | android { 15 | compilations.all { 16 | kotlinOptions { 17 | jvmTarget = "1.8" 18 | } 19 | } 20 | } 21 | 22 | listOf( 23 | iosX64(), 24 | iosArm64(), 25 | iosSimulatorArm64() 26 | ).forEach { 27 | it.binaries.framework { 28 | baseName = "api" 29 | } 30 | } 31 | 32 | sourceSets { 33 | 34 | val commonMain by getting { 35 | dependencies { 36 | implementation(project(":libraries:core")) 37 | implementation(project(":libraries:network")) 38 | } 39 | } 40 | } 41 | } 42 | 43 | android { 44 | namespace = "com.utsman.tokobola.api" 45 | compileSdk = 33 46 | defaultConfig { 47 | minSdk = 24 48 | } 49 | } 50 | 51 | buildkonfig { 52 | packageName = "com.utsman.tokobola.api" 53 | 54 | // default config is required 55 | defaultConfigs { 56 | buildConfigField(STRING, "BASE_URL", project.properties.get("base.url").toString()) 57 | buildConfigField(STRING, "MAPBOX_BASE_URL", project.properties.get("mapbox.base.url").toString()) 58 | } 59 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/tab/ExploreTab.kt: -------------------------------------------------------------------------------- 1 | package tab 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.painter.Painter 10 | import androidx.compose.ui.unit.dp 11 | import cafe.adriel.voyager.navigator.tab.Tab 12 | import cafe.adriel.voyager.navigator.tab.TabOptions 13 | import com.utsman.tokobola.core.navigation.LocalNavigation 14 | import com.utsman.tokobola.core.navigation.LocalScreenContainer 15 | import com.utsman.tokobola.resources.SharedRes 16 | import dev.icerock.moko.resources.compose.painterResource 17 | 18 | internal object ExploreTab : CustomTab { 19 | 20 | @Composable 21 | override fun Content() { 22 | val screenContainer = LocalScreenContainer.current 23 | screenContainer.explore().Content() 24 | } 25 | 26 | override val options: TabOptions 27 | @Composable 28 | get() { 29 | val title = "Explorer" 30 | val painter = painterResource(SharedRes.images.icon_explore) 31 | return remember { 32 | TabOptions( 33 | index = 1u, 34 | title = title, 35 | icon = painter 36 | ) 37 | } 38 | } 39 | 40 | override val iconSelected: Painter? 41 | @Composable 42 | get() { 43 | return painterResource(SharedRes.images.icon_explore_fill) 44 | } 45 | } -------------------------------------------------------------------------------- /features/cart/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | id("com.android.library") 6 | id("org.jetbrains.compose") 7 | kotlin("plugin.serialization") version "1.8.21" 8 | id("com.codingfeline.buildkonfig") 9 | } 10 | 11 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) 12 | kotlin { 13 | targetHierarchy.default() 14 | 15 | android { 16 | compilations.all { 17 | kotlinOptions { 18 | jvmTarget = "1.8" 19 | } 20 | } 21 | } 22 | 23 | listOf( 24 | iosX64(), 25 | iosArm64(), 26 | iosSimulatorArm64() 27 | ).forEach { 28 | it.binaries.framework { 29 | baseName = "cart" 30 | } 31 | } 32 | 33 | sourceSets { 34 | val commonMain by getting { 35 | dependencies { 36 | implementation(project(":libraries:core")) 37 | implementation(project(":libraries:network")) 38 | implementation(project(":libraries:database")) 39 | implementation(project(":libraries:location")) 40 | 41 | implementation(project(":libraries:common")) 42 | } 43 | } 44 | } 45 | } 46 | 47 | android { 48 | namespace = "com.utsman.tokobola.cart" 49 | compileSdk = 33 50 | defaultConfig { 51 | minSdk = 24 52 | } 53 | } 54 | 55 | buildkonfig { 56 | packageName = "com.utsman.tokobola.cart" 57 | 58 | // default config is required 59 | defaultConfigs { 60 | buildConfigField(STRING, "MAPBOX_TOKEN", project.properties.get("mapbox.token").toString()) 61 | } 62 | } -------------------------------------------------------------------------------- /features/details/src/commonMain/kotlin/com/utsman/tokobola/details/domain/DetailRepository.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.details.domain 2 | 3 | import com.utsman.tokobola.api.productWebApi 4 | import com.utsman.tokobola.core.SingletonCreator 5 | import com.utsman.tokobola.database.data.RecentlyViewedRealm 6 | import com.utsman.tokobola.database.data.WishlistRealm 7 | import com.utsman.tokobola.database.localRepository 8 | 9 | class DetailRepository { 10 | 11 | private val productApi by productWebApi() 12 | private val localRepository by localRepository() 13 | 14 | suspend fun getDetailProduct(productId: Int) = productApi.getDetail(productId) 15 | 16 | suspend fun markAsViewed(productId: Int) { 17 | localRepository.insertRecentlyViewed(RecentlyViewedRealm().apply { 18 | this.productId = productId 19 | }) 20 | } 21 | 22 | suspend fun getCartProduct(productId: Int) = localRepository.getProductCart(productId) 23 | 24 | suspend fun insertOrUpdateCart(productId: Int, operationQuantity: (Int) -> Int) { 25 | localRepository.insertOrUpdateProductCart(productId, operationQuantity) 26 | } 27 | 28 | suspend fun getProductCategory(categoryId: Int, page: Int) = 29 | productApi.getByCategoryPaged(categoryId, page) 30 | 31 | suspend fun getProductBrand(brandId: Int, page: Int) = productApi.getByBrandPaged(brandId, page) 32 | 33 | suspend fun toggleWishlist(productId: Int) { 34 | localRepository.toggleWishlist(WishlistRealm().apply { 35 | this.productId = productId 36 | }) 37 | } 38 | suspend fun isWishlistExist(productId: Int) = localRepository.checkWishlistIsExist(productId) 39 | 40 | companion object : SingletonCreator() 41 | } -------------------------------------------------------------------------------- /features/cart/src/commonMain/kotlin/com/utsman/tokobola/cart/ui/LocationPickerViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.cart.ui 2 | 3 | import com.utsman.tokobola.cart.domain.LocationPickerUseCase 4 | import com.utsman.tokobola.common.entity.LocationPlace 5 | import com.utsman.tokobola.core.ViewModel 6 | import com.utsman.tokobola.core.data.LatLon 7 | import kotlinx.coroutines.flow.collectLatest 8 | import kotlinx.coroutines.flow.debounce 9 | import kotlinx.coroutines.flow.distinctUntilChanged 10 | import kotlinx.coroutines.launch 11 | 12 | class LocationPickerViewModel(private val useCase: LocationPickerUseCase) : ViewModel() { 13 | 14 | val query get() = useCase.query 15 | val locationResultState = useCase.locationSearchReducer.dataFlow 16 | val locationReverseState = useCase.locationReverseReducer.dataFlow 17 | 18 | private var proximityLatLon: LatLon? = null 19 | 20 | fun listenQuery() = viewModelScope.launch { 21 | query.debounce(2000) 22 | .distinctUntilChanged() 23 | .collectLatest { 24 | proximityLatLon?.let { latLon -> 25 | useCase.searchLocationPlace(it, latLon) 26 | } 27 | } 28 | } 29 | 30 | fun updateProximityLatLon(latLon: LatLon) { 31 | proximityLatLon = latLon 32 | } 33 | 34 | fun getLocationReverse(latLon: LatLon) = viewModelScope.launch { 35 | useCase.getLocationReverse(latLon) 36 | } 37 | 38 | fun insertShippingAddress(locationPlace: LocationPlace) = viewModelScope.launch { 39 | useCase.saveShippingAddress(locationPlace) 40 | } 41 | 42 | fun clearData() { 43 | useCase.clearData() 44 | } 45 | 46 | override fun onCleared() { 47 | clearData() 48 | super.onCleared() 49 | } 50 | } -------------------------------------------------------------------------------- /libraries/network/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("com.android.library") 4 | kotlin("plugin.serialization") version "1.8.21" 5 | } 6 | 7 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) 8 | kotlin { 9 | targetHierarchy.default() 10 | 11 | android { 12 | compilations.all { 13 | kotlinOptions { 14 | jvmTarget = "1.8" 15 | } 16 | } 17 | } 18 | 19 | listOf( 20 | iosX64(), 21 | iosArm64(), 22 | iosSimulatorArm64() 23 | ).forEach { 24 | it.binaries.framework { 25 | baseName = "network" 26 | } 27 | } 28 | 29 | sourceSets { 30 | val ktorVersion = "2.3.2" 31 | 32 | val commonMain by getting { 33 | dependencies { 34 | implementation("io.ktor:ktor-client-core:$ktorVersion") 35 | implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") 36 | implementation("io.ktor:ktor-client-logging:$ktorVersion") 37 | api("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") 38 | 39 | implementation(project(":libraries:core")) 40 | } 41 | } 42 | getByName("androidMain").dependencies { 43 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") 44 | implementation("io.ktor:ktor-client-okhttp:$ktorVersion") 45 | } 46 | 47 | getByName("iosMain").dependencies { 48 | implementation("io.ktor:ktor-client-darwin:$ktorVersion") 49 | } 50 | } 51 | } 52 | 53 | android { 54 | namespace = "com.utsman.tokobola.network" 55 | compileSdk = 33 56 | defaultConfig { 57 | minSdk = 24 58 | } 59 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TokoBola 2 | 3 | An online football store simulation application for Android and iOS, developed using the latest 4 | technology from Kotlin Multiplatform and Compose Multiplatform as the UI framework. 5 | 6 | This is a playground project and not serious. This project is being worked on periodically, with the 7 | goal of learning Kotlin Multiplatform, Compose for Multiplatform for Android and iOS, and many other 8 | things. I chose the marketplace theme because by developing this themed application, there's so much 9 | that can be learned, starting from architecture, local database, up to maps. 10 | 11 | 12 | | ![](doc/ss5.png) | ![](doc/ss6.png) | 13 | |--------------------|--------------------| 14 | | ![](doc/ss3.png) | ![](doc/ss4.png) | 15 | | ![](doc/ss1.png) | ![](doc/ss2.png) | 16 | 17 | 18 | ## Architecture 19 | 20 | This main architecture combines MVVM with state management, and I call it MVVM + State. This state 21 | is not like MVI, which has multiple states for each flow. I only create 4 conditions, namely Idle, 22 | Loading, Success, and Failure. These states are propagated from data sources to nodes to be rendered 23 | according to their respective needs. 24 | 25 |

26 | 27 |

28 | 29 | ## Third party library 30 | 31 | - [Ktor client](https://ktor.io/docs/getting-started-ktor-client-multiplatform-mobile.html) 32 | - [moko-resources](https://github.com/icerockdev/moko-resources) 33 | - [moko-geo](https://github.com/icerockdev/moko-geo) 34 | - [Voyager](https://voyager.adriel.cafe/) 35 | - [Compose ImageLoader](https://github.com/qdsfdhvh/compose-imageloader) 36 | - [Realm Kotlin](https://realm.io/realm-kotlin/) 37 | - [BuildKonfig](https://github.com/yshrsmz/BuildKonfig) 38 | - [Mapbox Android SDK](https://docs.mapbox.com/android/maps/guides/) 39 | 40 | --- -------------------------------------------------------------------------------- /features/explore/src/commonMain/kotlin/com/utsman/tokobola/explore/domain/search/SearchUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.explore.domain.search 2 | 3 | import com.utsman.tokobola.common.entity.ThumbnailProduct 4 | import com.utsman.tokobola.common.toThumbnailProduct 5 | import com.utsman.tokobola.core.SingletonCreator 6 | import com.utsman.tokobola.core.data.Paged 7 | import com.utsman.tokobola.explore.domain.ExploreRepository 8 | import com.utsman.tokobola.network.ApiReducer 9 | import com.utsman.tokobola.network.AutoPagingAdapter 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | 12 | 13 | class SearchUseCase(private val repository: ExploreRepository) { 14 | 15 | val productSearchReducer = ApiReducer>() 16 | 17 | private val searchPagingAdapter = AutoPagingAdapter(productSearchReducer) 18 | 19 | val query = MutableStateFlow("") 20 | private var currentQuery = "" 21 | 22 | suspend fun getBySearch(query: String) { 23 | if (currentQuery != query) { 24 | currentQuery = query 25 | searchPagingAdapter.clear() 26 | } 27 | 28 | when { 29 | (query.count() < 3) -> { 30 | currentQuery = "" 31 | searchPagingAdapter.clear() 32 | } 33 | else -> { 34 | searchPagingAdapter.executeResponse( 35 | call = { 36 | repository.getProductBySearch(it, query) 37 | }, 38 | mapper = { 39 | it.toThumbnailProduct() 40 | } 41 | ) 42 | } 43 | } 44 | } 45 | 46 | fun clearData() { 47 | productSearchReducer.clear() 48 | query.value = "" 49 | } 50 | 51 | companion object : SingletonCreator() 52 | } -------------------------------------------------------------------------------- /androidApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/tab/AboutTab.kt: -------------------------------------------------------------------------------- 1 | package tab 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.painter.Painter 10 | import androidx.compose.ui.unit.dp 11 | import cafe.adriel.voyager.navigator.tab.TabOptions 12 | import com.utsman.tokobola.core.navigation.LocalNavigation 13 | import com.utsman.tokobola.core.navigation.LocalScreenContainer 14 | import com.utsman.tokobola.resources.SharedRes 15 | import dev.icerock.moko.resources.compose.painterResource 16 | 17 | 18 | /** 19 | * must be internal 20 | * https://github.com/JetBrains/compose-multiplatform/issues/3175#issuecomment-1564546150 21 | * */ 22 | internal object AboutTab : CustomTab { 23 | 24 | @Composable 25 | override fun Content() { 26 | val screenContainer = LocalScreenContainer.current 27 | val navigation = LocalNavigation.current 28 | Text("about", modifier = Modifier.padding(100.dp).clickable { 29 | navigation.goToDetailProduct(3) 30 | }) 31 | } 32 | 33 | override val options: TabOptions 34 | @Composable 35 | get() { 36 | val title = "About" 37 | val painter = painterResource(SharedRes.images.icon_about) 38 | return remember { 39 | TabOptions( 40 | index = 1u, 41 | title = title, 42 | icon = painter 43 | ) 44 | } 45 | } 46 | 47 | override val iconSelected: Painter? 48 | @Composable 49 | get() { 50 | return painterResource(SharedRes.images.icon_about_fill) 51 | } 52 | } -------------------------------------------------------------------------------- /libraries/core/src/commonMain/kotlin/com/utsman/tokobola/core/utils/MainExtensions.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalTime::class) 2 | 3 | package com.utsman.tokobola.core.utils 4 | 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.platform.LocalDensity 8 | import com.utsman.tokobola.resources.MokoColor 9 | import dev.icerock.moko.graphics.parseColor 10 | import io.ktor.util.date.getTimeMillis 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.Dispatchers 13 | import kotlinx.coroutines.IO 14 | import kotlinx.coroutines.SupervisorJob 15 | import kotlinx.coroutines.async 16 | import kotlinx.coroutines.awaitAll 17 | import kotlinx.coroutines.withContext 18 | import kotlin.coroutines.CoroutineContext 19 | import kotlin.time.ExperimentalTime 20 | 21 | fun Double.currency(): String { 22 | return "$$this" 23 | } 24 | 25 | fun Color.Companion.parseString(hex: String): Color { 26 | return Color(MokoColor.parseColor(hex).argb) 27 | } 28 | 29 | fun DefaultScope(): CoroutineScope = object : CoroutineScope { 30 | override val coroutineContext: CoroutineContext 31 | get() = SupervisorJob() + Dispatchers.Default 32 | } 33 | 34 | fun IoScope(): CoroutineScope = object : CoroutineScope { 35 | override val coroutineContext: CoroutineContext 36 | get() = SupervisorJob() + Dispatchers.IO 37 | } 38 | 39 | suspend fun Iterable.pmap(mapper: suspend (T) -> U): List = withContext(Dispatchers.IO) { 40 | map { async { mapper.invoke(it) } }.awaitAll() 41 | } 42 | 43 | fun nowMillis(): Long { 44 | return getTimeMillis() 45 | } 46 | 47 | suspend fun asyncAwait(action: suspend () -> T): T { 48 | return withContext(Dispatchers.IO) { 49 | action.invoke() 50 | } 51 | } 52 | 53 | @Composable 54 | fun Int.pxToDp() = with(LocalDensity.current) { 55 | this@pxToDp.toDp() 56 | } -------------------------------------------------------------------------------- /libraries/common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | id("com.android.library") 6 | id("org.jetbrains.compose") 7 | kotlin("plugin.serialization") version "1.8.21" 8 | id("com.codingfeline.buildkonfig") 9 | } 10 | 11 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) 12 | kotlin { 13 | targetHierarchy.default() 14 | 15 | android { 16 | compilations.all { 17 | kotlinOptions { 18 | jvmTarget = "1.8" 19 | } 20 | } 21 | } 22 | 23 | listOf( 24 | iosX64(), 25 | iosArm64(), 26 | iosSimulatorArm64() 27 | ).forEach { 28 | it.binaries.framework { 29 | baseName = "common" 30 | } 31 | } 32 | 33 | sourceSets { 34 | val commonMain by getting { 35 | dependencies { 36 | implementation(project(":libraries:core")) 37 | implementation(project(":libraries:network")) 38 | implementation(project(":libraries:database")) 39 | implementation(project(":libraries:location")) 40 | api(project(":libraries:api")) 41 | } 42 | } 43 | 44 | getByName("androidMain") { 45 | dependencies { 46 | implementation("com.mapbox.maps:android:10.15.0") 47 | } 48 | } 49 | } 50 | } 51 | 52 | android { 53 | namespace = "com.utsman.tokobola.feature.common" 54 | compileSdk = 33 55 | defaultConfig { 56 | minSdk = 24 57 | } 58 | } 59 | 60 | buildkonfig { 61 | packageName = "com.utsman.tokobola.common" 62 | 63 | // default config is required 64 | defaultConfigs { 65 | buildConfigField(STRING, "MAPBOX_TOKEN", project.properties.get("mapbox.token").toString()) 66 | } 67 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/ScreenContainerProvider.kt: -------------------------------------------------------------------------------- 1 | import cafe.adriel.voyager.core.screen.Screen 2 | import com.utsman.tokobola.cart.ui.Cart 3 | import com.utsman.tokobola.cart.ui.LocationPicker 4 | import com.utsman.tokobola.core.data.LatLon 5 | import com.utsman.tokobola.core.navigation.ScreenContainer 6 | import com.utsman.tokobola.core.navigation.screenContentOf 7 | import com.utsman.tokobola.details.ui.product.ProductDetail 8 | import com.utsman.tokobola.details.ui.brand.BrandDetail 9 | import com.utsman.tokobola.details.ui.category.CategoryDetail 10 | import com.utsman.tokobola.explore.ui.explore.Explore 11 | import com.utsman.tokobola.explore.ui.search.Search 12 | import com.utsman.tokobola.home.ui.Home 13 | import com.utsman.tokobola.wishlist.ui.Wishlist 14 | 15 | class ScreenContainerProvider : ScreenContainer { 16 | 17 | override fun home(): Screen = screenContentOf("home") { 18 | Home() 19 | } 20 | 21 | override fun detailProduct(productId: Int): Screen = screenContentOf("detail") { 22 | ProductDetail(productId) 23 | } 24 | 25 | override fun detailCategory(categoryId: Int) = screenContentOf("category_detail") { 26 | CategoryDetail(categoryId) 27 | } 28 | 29 | override fun detailBrand(brandId: Int) = screenContentOf("brand_detail") { 30 | BrandDetail(brandId) 31 | } 32 | 33 | override fun explore(): Screen = screenContentOf("explore") { 34 | Explore() 35 | } 36 | 37 | override fun wishlist(): Screen = screenContentOf("wishlist") { 38 | Wishlist() 39 | } 40 | 41 | override fun search(): Screen = screenContentOf("search") { 42 | Search() 43 | } 44 | 45 | override fun cart(): Screen = screenContentOf("cart") { 46 | Cart() 47 | } 48 | 49 | override fun locationPicker(latLon: LatLon) = screenContentOf("location_picker") { 50 | LocationPicker(latLon) 51 | } 52 | } -------------------------------------------------------------------------------- /resources/resources.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'resources' 3 | spec.version = '1.0.0' 4 | spec.homepage = 'Link to the Shared Module homepage' 5 | spec.source = { :http=> ''} 6 | spec.authors = '' 7 | spec.license = '' 8 | spec.summary = 'Some description for the Shared Module' 9 | spec.vendored_frameworks = 'build/cocoapods/framework/shared.framework' 10 | spec.libraries = 'c++' 11 | spec.ios.deployment_target = '14.1' 12 | 13 | 14 | spec.pod_target_xcconfig = { 15 | 'KOTLIN_PROJECT_PATH' => ':resources', 16 | 'PRODUCT_MODULE_NAME' => 'shared', 17 | } 18 | 19 | spec.script_phases = [ 20 | { 21 | :name => 'Build resources', 22 | :execution_position => :before_compile, 23 | :shell_path => '/bin/sh', 24 | :script => <<-SCRIPT 25 | if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then 26 | echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"" 27 | exit 0 28 | fi 29 | set -ev 30 | REPO_ROOT="$PODS_TARGET_SRCROOT" 31 | "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \ 32 | -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \ 33 | -Pkotlin.native.cocoapods.archs="$ARCHS" \ 34 | -Pkotlin.native.cocoapods.configuration="$CONFIGURATION" 35 | SCRIPT 36 | } 37 | ] 38 | spec.resources = ['src/commonMain/resources/**', 'src/iosMain/resources/**'] 39 | spec.exclude_files = ['src/commonMain/resources/MR/**'] 40 | end -------------------------------------------------------------------------------- /iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIViewControllerBasedStatusBarAppearance 6 | 7 | NSLocationWhenInUseUsageDescription 8 | 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | CADisableMinimumFrameDurationOnPhone 28 | 29 | UIApplicationSceneManifest 30 | 31 | UIApplicationSupportsMultipleScenes 32 | 33 | 34 | UILaunchScreen 35 | 36 | UIRequiredDeviceCapabilities 37 | 38 | armv7 39 | 40 | UISupportedInterfaceOrientations 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UISupportedInterfaceOrientations~ipad 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationPortraitUpsideDown 50 | UIInterfaceOrientationLandscapeLeft 51 | UIInterfaceOrientationLandscapeRight 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /features/explore/src/commonMain/kotlin/com/utsman/tokobola/explore/ui/explore/ExploreViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.explore.ui.explore 2 | 3 | import com.utsman.tokobola.core.ViewModel 4 | import com.utsman.tokobola.explore.domain.explore.ExploreUseCase 5 | import com.utsman.tokobola.explore.ui.ExploreUiConfig 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.launch 8 | 9 | class ExploreViewModel(private val useCase: ExploreUseCase) : ViewModel() { 10 | 11 | val brandState get() = useCase.brandReducer.dataFlow 12 | val categoryState get() = useCase.categoryReducer.dataFlow 13 | 14 | val productBrandState get() = useCase.productBrandReducer.dataFlow 15 | val productCategoryState get() = useCase.productCategoryReducer.dataFlow 16 | 17 | val topProductState get() = useCase.topProductReducer.dataFlow 18 | val curatedProductState get() = useCase.curatedProductReducer.dataFlow 19 | 20 | val uiConfig = MutableStateFlow(ExploreUiConfig()) 21 | 22 | fun getBrand() = viewModelScope.launch { 23 | useCase.getBrand() 24 | } 25 | 26 | fun getCategory() = viewModelScope.launch { 27 | useCase.getCategory() 28 | } 29 | 30 | fun getProductBrand(brandId: Int) = viewModelScope.launch { 31 | useCase.getProductBrand(brandId) 32 | } 33 | fun getProductCategory(categoryId: Int) = viewModelScope.launch { 34 | useCase.getProductCategory(categoryId) 35 | } 36 | 37 | fun getTopProduct() = viewModelScope.launch { 38 | useCase.getTopProduct() 39 | } 40 | 41 | fun getCuratedProduct() = viewModelScope.launch { 42 | useCase.getCuratedProduct() 43 | } 44 | 45 | fun updateUiConfig(uiConfig: () -> ExploreUiConfig) { 46 | val newUiConfig = uiConfig.invoke() 47 | this.uiConfig.value = newUiConfig 48 | } 49 | 50 | fun restartData() { 51 | useCase.clearData() 52 | getBrand() 53 | getCategory() 54 | } 55 | } -------------------------------------------------------------------------------- /libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/AutoPagingAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.network 2 | 3 | import com.utsman.tokobola.core.data.Paged 4 | import com.utsman.tokobola.core.data.orFalse 5 | import com.utsman.tokobola.core.data.orNol 6 | import com.utsman.tokobola.network.response.BasePagedResponse 7 | 8 | 9 | class AutoPagingAdapter(val pagingReducer: ApiReducer>) { 10 | 11 | val listPaged: MutableList = mutableListOf() 12 | var currentPage: Int = 1 13 | var prevPage: Int = 1 14 | var hasNextPage = true 15 | 16 | suspend inline fun executeResponse(crossinline call: suspend (Int) -> BasePagedResponse, crossinline mapper: (T) -> U) { 17 | if (hasNextPage) { 18 | pagingReducer.transform( 19 | call = { 20 | val pagingResponse = call.invoke(currentPage).also { 21 | hasNextPage = it.data?.hasNextPage.orFalse() 22 | prevPage = currentPage 23 | } 24 | pagingResponse 25 | }, 26 | mapper = { pagedResponse -> 27 | val dataPaged = pagedResponse.data 28 | val dataList = dataPaged?.data.orEmpty() 29 | .map(mapper) 30 | 31 | currentPage = dataPaged?.page.orNol() + 1 32 | 33 | listPaged.addAll(dataList) 34 | Paged( 35 | data = listPaged, 36 | hasNextPage = dataPaged?.hasNextPage.orFalse(), 37 | page = dataPaged?.page ?: 1, 38 | perPage = dataPaged?.perPage ?: 10 39 | ) 40 | } 41 | ) 42 | } else { 43 | println("End of reach paging!") 44 | } 45 | } 46 | 47 | fun clear() { 48 | currentPage = 1 49 | prevPage = 0 50 | hasNextPage = true 51 | listPaged.clear() 52 | pagingReducer.clear() 53 | } 54 | } -------------------------------------------------------------------------------- /features/home/src/commonMain/kotlin/com/utsman/tokobola/home/ui/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.home.ui 2 | 3 | import com.utsman.tokobola.common.entity.Brand 4 | import com.utsman.tokobola.common.entity.ThumbnailProduct 5 | import com.utsman.tokobola.core.ViewModel 6 | import com.utsman.tokobola.home.domain.HomeUseCase 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.launch 9 | 10 | class HomeViewModel(private val homeUseCase: HomeUseCase) : ViewModel() { 11 | 12 | val productsFeaturedState get() = homeUseCase.productsFeaturedReducer.dataFlow 13 | val homeBannerState get() = homeUseCase.productBannerReducer.dataFlow 14 | val brandState get() = homeUseCase.brandReducer.dataFlow 15 | 16 | val productViewed get() = homeUseCase.productViewedReducer.dataFlow 17 | 18 | val productsFeaturedFlow = MutableStateFlow(emptyList()) 19 | val brandListFlow = MutableStateFlow(emptyList()) 20 | 21 | val isRestart = MutableStateFlow(false) 22 | 23 | fun getHomeProduct() = viewModelScope.launch { 24 | isRestart.value = true 25 | homeUseCase.getProduct() 26 | } 27 | 28 | fun getHomeBanner() = viewModelScope.launch { 29 | homeUseCase.getBanner() 30 | } 31 | 32 | fun getProductViewed() = viewModelScope.launch { 33 | homeUseCase.getAllProductViewed() 34 | } 35 | 36 | fun getBrand() = viewModelScope.launch { 37 | homeUseCase.getBrand() 38 | } 39 | 40 | fun restartData() { 41 | isRestart.value = true 42 | productsFeaturedFlow.value = emptyList() 43 | brandListFlow.value = emptyList() 44 | homeUseCase.clearProductPage() 45 | getHomeBanner() 46 | getHomeProduct() 47 | getBrand() 48 | getProductViewed() 49 | } 50 | 51 | fun postProduct(list: List) { 52 | isRestart.value = false 53 | productsFeaturedFlow.value = list 54 | } 55 | 56 | fun postBrandList(list: List) { 57 | brandListFlow.value = list 58 | } 59 | } -------------------------------------------------------------------------------- /resources/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | kotlin("native.cocoapods") 4 | id("com.android.library") 5 | id("org.jetbrains.compose") 6 | id("dev.icerock.mobile.multiplatform-resources") 7 | } 8 | 9 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) 10 | kotlin { 11 | targetHierarchy.default() 12 | 13 | android { 14 | compilations.all { 15 | kotlinOptions { 16 | jvmTarget = "1.8" 17 | } 18 | } 19 | } 20 | 21 | cocoapods { 22 | version = "1.0.0" 23 | summary = "Some description for the Shared Module" 24 | homepage = "Link to the Shared Module homepage" 25 | ios.deploymentTarget = "14.1" 26 | podfile = project.file("../iosApp/Podfile") 27 | framework { 28 | baseName = "shared" 29 | isStatic = true 30 | export("dev.icerock.moko:resources:0.23.0") 31 | export("dev.icerock.moko:graphics:0.9.0") // toUIColor here 32 | } 33 | extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']" 34 | extraSpecAttributes["exclude_files"] = "['src/commonMain/resources/MR/**']" 35 | } 36 | 37 | listOf( 38 | iosX64(), 39 | iosArm64(), 40 | iosSimulatorArm64() 41 | ).forEach { 42 | it.binaries.framework { 43 | baseName = "resources" 44 | } 45 | } 46 | 47 | sourceSets { 48 | val commonMain by getting { 49 | dependencies { 50 | api("dev.icerock.moko:resources-compose:0.23.0") 51 | } 52 | } 53 | } 54 | } 55 | 56 | multiplatformResources { 57 | multiplatformResourcesPackage = "com.utsman.tokobola.resources" 58 | multiplatformResourcesClassName = "SharedRes" 59 | 60 | } 61 | 62 | android { 63 | namespace = "com.utsman.tokobola.resources" 64 | sourceSets["main"].resources.srcDirs("src/commonMain/resources/MR/**") 65 | 66 | compileSdk = 33 67 | defaultConfig { 68 | minSdk = 24 69 | } 70 | } -------------------------------------------------------------------------------- /features/details/src/commonMain/kotlin/com/utsman/tokobola/details/ui/product/ProductDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.details.ui.product 2 | 3 | import com.utsman.tokobola.core.ViewModel 4 | import com.utsman.tokobola.details.domain.ProductDetailUseCase 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.asStateFlow 7 | import kotlinx.coroutines.launch 8 | 9 | class ProductDetailViewModel(private val productDetailUseCase: ProductDetailUseCase) : ViewModel() { 10 | 11 | val detailState get() = productDetailUseCase.productDetailReducer.dataFlow.asStateFlow() 12 | val wishlistState get() = productDetailUseCase.isProductExist.asStateFlow() 13 | 14 | val uiConfig = MutableStateFlow(ProductDetailUiConfig()) 15 | 16 | val productCart get() = productDetailUseCase.productCart.asStateFlow() 17 | 18 | fun getDetail(productId: Int) = viewModelScope.launch { 19 | productDetailUseCase.getDetail(productId) 20 | } 21 | 22 | fun postProductViewed(productId: Int) = viewModelScope.launch { 23 | productDetailUseCase.markProductViewed(productId) 24 | } 25 | 26 | fun updateUiConfig(uiConfig: () -> ProductDetailUiConfig) { 27 | val newUiConfig = uiConfig.invoke() 28 | this.uiConfig.value = newUiConfig 29 | } 30 | 31 | fun getCart(productId: Int) = viewModelScope.launch { 32 | productDetailUseCase.getProductCart(productId) 33 | } 34 | 35 | fun incrementCart(productId: Int) = viewModelScope.launch { 36 | productDetailUseCase.incrementCart(productId) 37 | } 38 | 39 | fun decrementCart(productId: Int) = viewModelScope.launch { 40 | productDetailUseCase.decrementCart(productId) 41 | } 42 | 43 | fun listenWishlist(productId: Int) = viewModelScope.launch { 44 | productDetailUseCase.listenIsWishlistExist(productId) 45 | } 46 | 47 | fun toggleWishlist(productId: Int) = viewModelScope.launch { 48 | productDetailUseCase.toggleWishlist(productId) 49 | } 50 | 51 | override fun onCleared() { 52 | viewModelScope.launch { 53 | productDetailUseCase.clearDetail() 54 | } 55 | super.onCleared() 56 | } 57 | } -------------------------------------------------------------------------------- /features/details/src/commonMain/kotlin/com/utsman/tokobola/details/domain/ProductDetailUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.details.domain 2 | 3 | import com.utsman.tokobola.common.entity.CartProduct 4 | import com.utsman.tokobola.common.mapToProduct 5 | import com.utsman.tokobola.common.entity.Product 6 | import com.utsman.tokobola.common.toEntity 7 | import com.utsman.tokobola.core.SingletonCreator 8 | import com.utsman.tokobola.network.ApiReducer 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.map 11 | 12 | class ProductDetailUseCase(private val repository: DetailRepository) { 13 | 14 | val productDetailReducer = ApiReducer() 15 | 16 | val productCart = MutableStateFlow(CartProduct()) 17 | 18 | val isProductExist = MutableStateFlow(false) 19 | 20 | suspend fun getDetail(productId: Int) { 21 | productDetailReducer.transform( 22 | call = { repository.getDetailProduct(productId) }, 23 | mapper = { 24 | it.data?.mapToProduct() ?: Product() 25 | } 26 | ) 27 | } 28 | 29 | suspend fun markProductViewed(productId: Int) { 30 | repository.markAsViewed(productId) 31 | } 32 | 33 | suspend fun incrementCart(productId: Int) { 34 | repository.insertOrUpdateCart(productId) { 35 | it + 1 36 | } 37 | } 38 | 39 | suspend fun decrementCart(productId: Int) { 40 | repository.insertOrUpdateCart(productId) { 41 | it - 1 42 | } 43 | } 44 | 45 | suspend fun getProductCart(productId: Int) { 46 | repository.getCartProduct(productId) 47 | .map { it?.toEntity() ?: CartProduct() } 48 | .collect { 49 | productCart.value = it 50 | } 51 | } 52 | 53 | suspend fun listenIsWishlistExist(productId: Int) { 54 | repository.isWishlistExist(productId) 55 | .collect { isExist -> 56 | isProductExist.value = isExist 57 | } 58 | } 59 | 60 | suspend fun toggleWishlist(productId: Int) { 61 | repository.toggleWishlist(productId) 62 | } 63 | 64 | suspend fun clearDetail() { 65 | productDetailReducer.clear() 66 | } 67 | 68 | companion object : SingletonCreator() 69 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.internal.impldep.org.jsoup.safety.Safelist.basic 2 | rootProject.name = "TokoBola" 3 | 4 | pluginManagement { 5 | repositories { 6 | gradlePluginPortal() 7 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 8 | google() 9 | } 10 | 11 | plugins { 12 | val kotlinVersion = extra["kotlin.version"] as String 13 | val agpVersion = extra["agp.version"] as String 14 | val composeVersion = extra["compose.version"] as String 15 | 16 | kotlin("jvm").version(kotlinVersion) 17 | kotlin("multiplatform").version(kotlinVersion) 18 | kotlin("android").version(kotlinVersion) 19 | 20 | id("com.android.application").version(agpVersion) 21 | id("com.android.library").version(agpVersion) 22 | 23 | id("org.jetbrains.compose").version(composeVersion) 24 | id("dev.icerock.mobile.multiplatform-resources").version("0.23.0") 25 | id("com.codingfeline.buildkonfig").version("0.13.3") 26 | } 27 | } 28 | 29 | dependencyResolutionManagement { 30 | repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) 31 | repositories { 32 | google() 33 | mavenCentral() 34 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 35 | maven { 36 | url = uri("https://api.mapbox.com/downloads/v2/releases/maven") 37 | authentication { 38 | create("basic") 39 | } 40 | credentials { 41 | // Do not change the username below. 42 | // This should always be `mapbox` (not your username). 43 | username = "mapbox" 44 | // Use the secret token you stored in gradle.properties as the password 45 | password = "sk.eyJ1Ijoia3VjaW5nYXBlcyIsImEiOiJjbGxmdnVtbGIwemdqM2txaHZtam5teGxiIn0.4AlEjvqBWJgaYOElmi5bxA" 46 | } 47 | } 48 | } 49 | } 50 | 51 | include(":androidApp") 52 | include(":shared") 53 | include(":resources") 54 | include(":libraries:core") 55 | include(":libraries:common") 56 | include(":libraries:network") 57 | include(":libraries:api") 58 | include(":libraries:database") 59 | include(":libraries:location") 60 | include(":features:home") 61 | include(":features:details") 62 | include(":features:explore") 63 | include(":features:wishlist") 64 | include(":features:cart") 65 | -------------------------------------------------------------------------------- /features/cart/src/commonMain/kotlin/com/utsman/tokobola/cart/domain/CartRepository.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.cart.domain 2 | 3 | import com.utsman.tokobola.api.mapboxWebApi 4 | import com.utsman.tokobola.api.productWebApi 5 | import com.utsman.tokobola.cart.BuildKonfig 6 | import com.utsman.tokobola.cart.ui.CartUiConfig 7 | import com.utsman.tokobola.common.entity.Cart 8 | import com.utsman.tokobola.common.entity.LocationPlace 9 | import com.utsman.tokobola.common.toRealm 10 | import com.utsman.tokobola.core.SingletonCreator 11 | import com.utsman.tokobola.core.data.LatLon 12 | import com.utsman.tokobola.database.data.CartProductRealm 13 | import com.utsman.tokobola.database.localRepository 14 | 15 | class CartRepository { 16 | private val productWebApi by productWebApi() 17 | private val mapboxWebApi by mapboxWebApi() 18 | private val localRepository by localRepository() 19 | 20 | suspend fun getThumbnailByIds(ids: List) = productWebApi.getThumbnailByIds(ids) 21 | 22 | suspend fun getAllCart() = localRepository.selectAllCart() 23 | 24 | suspend fun replaceCart(list: List) { 25 | val cartProductRealm = list.map { 26 | CartProductRealm().apply { 27 | productId = it.product.id 28 | quantity = it.quantity 29 | millis = it.millis 30 | } 31 | } 32 | 33 | localRepository.replaceAllCart(cartProductRealm) 34 | } 35 | 36 | suspend fun getLocationPlace(latLon: LatLon) = mapboxWebApi.getReverseGeocoding(latLon, BuildKonfig.MAPBOX_TOKEN) 37 | suspend fun searchLocationPlace(query: String, latLon: LatLon) = mapboxWebApi.getSearchGeocoding(query, latLon, BuildKonfig.MAPBOX_TOKEN) 38 | 39 | suspend fun getLocalCurrentLocationPlace() = localRepository.getLocationPlace(CartUiConfig.KEY_LOCATION_CURRENT) 40 | suspend fun getShippingLocationPlace() = localRepository.getLocationPlace(CartUiConfig.KEY_LOCATION_SHIPPING) 41 | suspend fun insertLocalCurrentLocationPlace(locationPlace: LocationPlace) { 42 | localRepository.insertOrUpdateLocationPlace(locationPlace.toRealm(CartUiConfig.KEY_LOCATION_CURRENT)) 43 | } 44 | 45 | suspend fun insertShippingLocationPlace(locationPlace: LocationPlace) { 46 | localRepository.insertOrUpdateLocationPlace(locationPlace.toRealm(CartUiConfig.KEY_LOCATION_SHIPPING)) 47 | } 48 | 49 | companion object : SingletonCreator() 50 | } -------------------------------------------------------------------------------- /libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/response/MapboxSearchResponse.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.api.response 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class MapboxSearchResponse( 9 | @SerialName("type") 10 | val type: String?, 11 | @SerialName("query") 12 | val query: List?, 13 | @SerialName("features") 14 | val features: List?, 15 | @SerialName("attribution") 16 | val attribution: String? 17 | ) { 18 | @Serializable 19 | data class FeatureResponse( 20 | @SerialName("id") 21 | val id: String?, 22 | @SerialName("type") 23 | val type: String?, 24 | @SerialName("place_type") 25 | val placeType: List?, 26 | @SerialName("relevance") 27 | val relevance: Double?, 28 | @SerialName("properties") 29 | val properties: PropertiesResponse?, 30 | @SerialName("text") 31 | val text: String?, 32 | @SerialName("place_name") 33 | val placeName: String?, 34 | @SerialName("center") 35 | val center: List?, 36 | @SerialName("geometry") 37 | val geometry: GeometryResponse?, 38 | @SerialName("context") 39 | val context: List? 40 | ) { 41 | @Serializable 42 | data class PropertiesResponse( 43 | @SerialName("foursquare") 44 | val foursquare: String?, 45 | @SerialName("landmark") 46 | val landmark: Boolean?, 47 | @SerialName("address") 48 | val address: String?, 49 | @SerialName("category") 50 | val category: String? 51 | ) 52 | 53 | @Serializable 54 | data class GeometryResponse( 55 | @SerialName("coordinates") 56 | val coordinates: List?, 57 | @SerialName("type") 58 | val type: String? 59 | ) 60 | 61 | @Serializable 62 | data class ContextResponse( 63 | @SerialName("id") 64 | val id: String?, 65 | @SerialName("mapbox_id") 66 | val mapboxId: String?, 67 | @SerialName("text") 68 | val text: String?, 69 | @SerialName("wikidata") 70 | val wikidata: String?, 71 | @SerialName("short_code") 72 | val shortCode: String? 73 | ) 74 | } 75 | } -------------------------------------------------------------------------------- /features/cart/src/commonMain/kotlin/com/utsman/tokobola/cart/ui/CartViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.cart.ui 2 | 3 | import com.utsman.tokobola.cart.domain.CartUseCase 4 | import com.utsman.tokobola.common.entity.Cart 5 | import com.utsman.tokobola.core.ViewModel 6 | import com.utsman.tokobola.location.LocationTrackerProvider 7 | import dev.icerock.moko.geo.LatLng 8 | import io.ktor.util.date.getTimeMillis 9 | import kotlinx.coroutines.CoroutineExceptionHandler 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.asStateFlow 12 | import kotlinx.coroutines.launch 13 | 14 | class CartViewModel(private val useCase: CartUseCase) : ViewModel() { 15 | 16 | val cartState get() = useCase.cartReducer.dataFlow.asStateFlow() 17 | 18 | val cartUiConfig: MutableStateFlow = MutableStateFlow(CartUiConfig()) 19 | 20 | val shippingLocationState = useCase.locationShippingReducer.dataFlow 21 | 22 | fun listenCart() = viewModelScope.launch { 23 | useCase.getCart() 24 | } 25 | 26 | fun pushCart(list: List) { 27 | cartUiConfig.value = CartUiConfig(list) 28 | } 29 | 30 | fun incrementCart(productId: Int) { 31 | val currentCart = cartUiConfig.value 32 | val newCart = updateQuantityInCart(currentCart.carts, productId) { 33 | it+1 34 | } 35 | 36 | cartUiConfig.value = currentCart.copy(carts = newCart, time = getTimeMillis()) 37 | } 38 | 39 | fun decrementCart(productId: Int) { 40 | val currentCart = cartUiConfig.value 41 | val newCart = updateQuantityInCart(currentCart.carts, productId) { 42 | it-1 43 | } 44 | cartUiConfig.value = currentCart.copy(carts = newCart, time = getTimeMillis()) 45 | } 46 | 47 | private fun updateQuantityInCart(cart: List, productIdToUpdate: Int, operation: (Int) -> Int): List { 48 | val newCart = cart.map { 49 | if (it.product.id == productIdToUpdate) { 50 | it.quantity = operation.invoke(it.quantity) 51 | } 52 | it 53 | } 54 | 55 | return newCart 56 | } 57 | 58 | fun getShippingLocation() = viewModelScope.launch { 59 | useCase.getShippingLocation() 60 | } 61 | 62 | override fun onCleared() { 63 | useCase.stopLocationPlace() 64 | viewModelScope.launch { 65 | useCase.updateCart(cartUiConfig.value.carts) 66 | } 67 | super.onCleared() 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/NavigationProvider.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.Composable 2 | import cafe.adriel.voyager.navigator.LocalNavigator 3 | import cafe.adriel.voyager.navigator.Navigator 4 | import com.utsman.tokobola.core.data.LatLon 5 | import com.utsman.tokobola.core.navigation.LocalScreenContainer 6 | import com.utsman.tokobola.core.navigation.Navigation 7 | import com.utsman.tokobola.core.navigation.ScreenContainer 8 | import kotlinx.coroutines.flow.MutableStateFlow 9 | 10 | class NavigationProvider : Navigation { 11 | 12 | private val navigatorStack = MutableStateFlow(null) 13 | private val screenContainer = MutableStateFlow(null) 14 | 15 | @Composable 16 | fun initialize() { 17 | navigatorStack.value = LocalNavigator.current 18 | screenContainer.value = LocalScreenContainer.current 19 | } 20 | 21 | override fun back(): Boolean { 22 | return tryAction { it.pop() } 23 | } 24 | 25 | override fun goToDetailProduct(id: Int): Boolean { 26 | return tryAction { nav -> 27 | screenContainer.value?.detailProduct(id)?.let { nav.push(it) } 28 | } 29 | } 30 | 31 | override fun goToDetailCategory(categoryId: Int): Boolean { 32 | return tryAction { nav -> 33 | screenContainer.value?.detailCategory(categoryId)?.let { nav.push(it) } 34 | } 35 | } 36 | 37 | override fun goToDetailBrand(brandId: Int): Boolean { 38 | return tryAction { nav -> 39 | screenContainer.value?.detailBrand(brandId)?.let { nav.push(it) } 40 | } 41 | } 42 | 43 | override fun goToSearch(): Boolean { 44 | return tryAction { nav -> 45 | screenContainer.value?.search()?.let { nav.push(it) } 46 | } 47 | } 48 | 49 | override fun goToCart(): Boolean { 50 | return tryAction { nav -> 51 | screenContainer.value?.cart()?.let { nav.push(it) } 52 | } 53 | } 54 | 55 | override fun goToLocationPicker(latLon: LatLon): Boolean { 56 | return tryAction { nav -> 57 | screenContainer.value?.locationPicker(latLon)?.let { nav.push(it) } 58 | } 59 | } 60 | 61 | private fun tryAction(action: (Navigator) -> Unit): Boolean { 62 | return try { 63 | navigatorStack.value?.let(action) 64 | true 65 | } catch (e: IllegalArgumentException) { 66 | e.printStackTrace() 67 | false 68 | } catch (e: Exception) { 69 | e.printStackTrace() 70 | false 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /libraries/core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("com.android.library") 4 | id("org.jetbrains.compose") 5 | kotlin("plugin.serialization") version "1.8.21" 6 | } 7 | 8 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) 9 | kotlin { 10 | targetHierarchy.default() 11 | 12 | android { 13 | compilations.all { 14 | kotlinOptions { 15 | jvmTarget = "1.8" 16 | } 17 | } 18 | } 19 | 20 | listOf( 21 | iosX64(), 22 | iosArm64(), 23 | iosSimulatorArm64() 24 | ).forEach { 25 | it.binaries.framework { 26 | baseName = "core" 27 | } 28 | } 29 | 30 | sourceSets { 31 | val lifecycleVersion = "2.6.1" 32 | val voyagerVersion = "1.0.0-rc05" 33 | 34 | val commonMain by getting { 35 | dependencies { 36 | api(compose.runtime) 37 | api(compose.foundation) 38 | api(compose.material) 39 | @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) 40 | api(compose.components.resources) 41 | api(compose.animation) 42 | 43 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") 44 | 45 | api(project(":resources")) 46 | 47 | api("io.github.qdsfdhvh:image-loader:1.6.0") 48 | api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") 49 | 50 | api("cafe.adriel.voyager:voyager-navigator:$voyagerVersion") 51 | api("cafe.adriel.voyager:voyager-transitions:$voyagerVersion") 52 | api("cafe.adriel.voyager:voyager-tab-navigator:$voyagerVersion") 53 | 54 | api("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") 55 | 56 | } 57 | } 58 | 59 | getByName("androidMain") { 60 | dependencies { 61 | api("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion") 62 | api("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion") 63 | 64 | api("androidx.activity:activity-compose:1.7.2") 65 | api("androidx.appcompat:appcompat:1.6.1") 66 | api("androidx.core:core-ktx:1.10.1") 67 | } 68 | } 69 | } 70 | } 71 | 72 | android { 73 | namespace = "com.utsman.tokobola.core" 74 | compileSdk = 33 75 | 76 | sourceSets["main"].resources.srcDirs("src/commonMain/resources") 77 | 78 | defaultConfig { 79 | minSdk = 24 80 | } 81 | } -------------------------------------------------------------------------------- /libraries/location/src/commonMain/kotlin/com/utsman/tokobola/location/LocationTracker.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.location 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.utsman.tokobola.core.SingletonCreator 5 | import com.utsman.tokobola.core.State 6 | import com.utsman.tokobola.core.data.LatLon 7 | import com.utsman.tokobola.core.utils.DefaultScope 8 | import dev.icerock.moko.geo.LocationTracker 9 | import dev.icerock.moko.geo.compose.BindLocationTrackerEffect 10 | import dev.icerock.moko.permissions.compose.BindEffect 11 | import kotlinx.coroutines.flow.SharingStarted 12 | import kotlinx.coroutines.flow.firstOrNull 13 | import kotlinx.coroutines.flow.map 14 | import kotlinx.coroutines.flow.onEach 15 | import kotlinx.coroutines.flow.stateIn 16 | import kotlinx.coroutines.flow.transform 17 | 18 | internal expect val locationTracker: LocationTracker 19 | 20 | private var lastKnowLocations: MutableList = mutableListOf() 21 | 22 | class LocationTrackerProvider { 23 | private val tracker = locationTracker 24 | 25 | var isHasStart: Boolean = false 26 | private set 27 | 28 | val locationFlow = tracker.getLocationsFlow() 29 | .map { LatLon(it.latitude, it.longitude) } 30 | .onEach { 31 | if (lastKnowLocations.isEmpty()) { 32 | lastKnowLocations.add(it) 33 | } 34 | } 35 | .stateIn( 36 | DefaultScope(), 37 | SharingStarted.Eagerly, 38 | null 39 | ) 40 | 41 | val locationStateFlow = locationFlow 42 | .transform { value -> 43 | if (value == null) { 44 | emit(State.Loading()) 45 | } else { 46 | emit(State.Success(value)) 47 | } 48 | } 49 | .stateIn( 50 | DefaultScope(), 51 | SharingStarted.Eagerly, 52 | State.Idle() 53 | ) 54 | 55 | @Composable 56 | fun bindComposable() { 57 | BindLocationTrackerEffect(tracker) 58 | BindEffect(tracker.permissionsController) 59 | } 60 | 61 | fun getLastKnownLocation(): LatLon? { 62 | return lastKnowLocations.firstOrNull() 63 | } 64 | 65 | suspend fun startTracking() { 66 | if (!isHasStart) { 67 | tracker.startTracking() 68 | isHasStart = true 69 | } 70 | } 71 | 72 | fun stopTracking() { 73 | tracker.stopTracking() 74 | isHasStart = false 75 | } 76 | 77 | suspend fun isEmptyQueue(): Boolean { 78 | return tracker.getLocationsFlow().firstOrNull() == null 79 | } 80 | 81 | companion object : SingletonCreator() 82 | } -------------------------------------------------------------------------------- /libraries/network/src/commonMain/kotlin/com/utsman/tokobola/network/StateTransformation.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.network 2 | 3 | import com.utsman.tokobola.core.State 4 | import io.ktor.client.plugins.ClientRequestException 5 | import kotlinx.serialization.encodeToString 6 | import kotlinx.serialization.json.Json 7 | import kotlinx.serialization.json.jsonObject 8 | 9 | interface StateTransformation { 10 | suspend fun transform(call: suspend () -> U, mapper: (U) -> T): State 11 | 12 | companion object { 13 | @Suppress("FunctionName") 14 | inline fun DefaultResponseTransform(): StateTransformation { 15 | val json = Json { 16 | ignoreUnknownKeys = true 17 | } 18 | 19 | return object : StateTransformation { 20 | override suspend fun transform(call: suspend () -> U, mapper: (U) -> T): State { 21 | val resultEvent: State = try { 22 | val dataSuccess = call.invoke() 23 | val jsonString = Json.encodeToString(dataSuccess) 24 | 25 | val jsonData = json.parseToJsonElement(jsonString) 26 | .jsonObject 27 | 28 | val status = jsonData["status"].toString().toBooleanStrict() 29 | val message = jsonData["message"].toString() 30 | 31 | if (status) { 32 | val dataResult = mapper.invoke(dataSuccess) 33 | State.Success(dataResult) 34 | } else { 35 | State.Failure(Throwable(message)) 36 | } 37 | } catch (e: ClientRequestException) { 38 | e.printStackTrace() 39 | State.Failure(e) 40 | } catch (e: IllegalArgumentException) { 41 | e.printStackTrace() 42 | State.Failure(e) 43 | } catch (e: Exception) { 44 | State.Failure(e) 45 | } 46 | 47 | return resultEvent 48 | } 49 | 50 | } 51 | } 52 | 53 | @Suppress("FunctionName") 54 | inline fun SimpleTransform(): StateTransformation { 55 | return object : StateTransformation { 56 | override suspend fun transform(call: suspend () -> U, mapper: (U) -> T): State { 57 | val data = call.invoke() 58 | return State.Success(mapper.invoke(data)) 59 | } 60 | 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/response/MapboxReverseResponse.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.api.response 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class MapboxReverseResponse( 9 | @SerialName("type") 10 | val type: String?, 11 | @SerialName("features") 12 | val features: List?, 13 | @SerialName("attribution") 14 | val attribution: String? 15 | ) { 16 | @Serializable 17 | data class FeatureResponse( 18 | @SerialName("id") 19 | val id: String?, 20 | @SerialName("type") 21 | val type: String?, 22 | @SerialName("place_type") 23 | val placeType: List?, 24 | @SerialName("relevance") 25 | val relevance: Int?, 26 | @SerialName("properties") 27 | val properties: PropertiesResponse?, 28 | @SerialName("text") 29 | val text: String?, 30 | @SerialName("place_name") 31 | val placeName: String?, 32 | @SerialName("center") 33 | val center: List?, 34 | @SerialName("geometry") 35 | val geometry: GeometryResponse?, 36 | @SerialName("context") 37 | val context: List?, 38 | @SerialName("bbox") 39 | val bbox: List? 40 | ) { 41 | @Serializable 42 | data class PropertiesResponse( 43 | @SerialName("foursquare") 44 | val foursquare: String?, 45 | @SerialName("landmark") 46 | val landmark: Boolean?, 47 | @SerialName("address") 48 | val address: String?, 49 | @SerialName("category") 50 | val category: String?, 51 | @SerialName("mapbox_id") 52 | val mapboxId: String?, 53 | @SerialName("wikidata") 54 | val wikidata: String?, 55 | @SerialName("short_code") 56 | val shortCode: String? 57 | ) 58 | 59 | @Serializable 60 | data class GeometryResponse( 61 | @SerialName("coordinates") 62 | val coordinates: List?, 63 | @SerialName("type") 64 | val type: String? 65 | ) 66 | 67 | @Serializable 68 | data class ContextResponse( 69 | @SerialName("id") 70 | val id: String?, 71 | @SerialName("mapbox_id") 72 | val mapboxId: String?, 73 | @SerialName("text") 74 | val text: String?, 75 | @SerialName("wikidata") 76 | val wikidata: String?, 77 | @SerialName("short_code") 78 | val shortCode: String? 79 | ) 80 | } 81 | } -------------------------------------------------------------------------------- /libraries/api/src/commonMain/kotlin/com/utsman/tokobola/api/ProductWebApi.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.api 2 | 3 | import com.utsman.tokobola.network.response.BasePagedResponse 4 | import com.utsman.tokobola.network.response.BaseResponse 5 | import com.utsman.tokobola.api.response.BrandResponse 6 | import com.utsman.tokobola.api.response.CategoryResponse 7 | import com.utsman.tokobola.api.response.HomeBannerResponse 8 | import com.utsman.tokobola.api.response.ProductResponse 9 | import com.utsman.tokobola.api.response.ThumbnailProductResponse 10 | import com.utsman.tokobola.core.SingletonCreator 11 | 12 | class ProductWebApi : WebDataSource() { 13 | 14 | suspend fun getByFeaturedPaged(page: Int): BasePagedResponse { 15 | return getPaged(WebEndPoint.PRODUCT_FEATURED.withParam("page", page)) 16 | } 17 | 18 | suspend fun getByBrandPaged(brandId: Int, page: Int): BasePagedResponse { 19 | return getPaged(WebEndPoint.PRODUCT_BRAND.withParam("page", page).withParam("brand_id", brandId)) 20 | } 21 | 22 | suspend fun getByCategoryPaged(categoryId: Int, page: Int): BasePagedResponse { 23 | return getPaged(WebEndPoint.PRODUCT_CATEGORY.withParam("page", page).withParam("category_id", categoryId)) 24 | } 25 | 26 | suspend fun getBySearch(page: Int, query: String): BasePagedResponse { 27 | return getPaged(WebEndPoint.PRODUCT_SEARCH.withParam("page", page).withParam("query", query)) 28 | } 29 | 30 | suspend fun getThumbnailByIds(ids: List): BaseResponse> { 31 | val newIds = ids.toString().replace("[", "").replace("]", "") 32 | .replace(" ", "") 33 | return get(WebEndPoint.PRODUCT_THUMBNAIL.withParam("id", newIds)) 34 | } 35 | 36 | suspend fun getTop(): BaseResponse> { 37 | return get(WebEndPoint.PRODUCT_TOP) 38 | } 39 | 40 | suspend fun getCurated(): BaseResponse> { 41 | return get(WebEndPoint.PRODUCT_CURATED) 42 | } 43 | 44 | suspend fun getDetail(productId: Int): BaseResponse { 45 | return get(WebEndPoint.PRODUCT_DETAIL.withParam("product_id", productId)) 46 | } 47 | 48 | suspend fun getHomeBanner(): BaseResponse> { 49 | return get(WebEndPoint.BANNER) 50 | } 51 | 52 | suspend fun getBrand(): BaseResponse> { 53 | return get(WebEndPoint.BRAND) 54 | } 55 | 56 | suspend fun getCategory() : BaseResponse> { 57 | return get(WebEndPoint.CATEGORY) 58 | } 59 | 60 | companion object : SingletonCreator() 61 | } -------------------------------------------------------------------------------- /features/cart/src/commonMain/kotlin/com/utsman/tokobola/cart/domain/LocationPickerUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.cart.domain 2 | 3 | import androidx.compose.runtime.derivedStateOf 4 | import com.utsman.tokobola.common.entity.LocationPlace 5 | import com.utsman.tokobola.common.toLocationPlace 6 | import com.utsman.tokobola.common.toThumbnailProduct 7 | import com.utsman.tokobola.core.SingletonCreator 8 | import com.utsman.tokobola.core.State 9 | import com.utsman.tokobola.core.data.LatLon 10 | import com.utsman.tokobola.location.LocationTrackerProvider 11 | import com.utsman.tokobola.network.ApiReducer 12 | import com.utsman.tokobola.network.StateTransformation 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.filterNotNull 15 | import kotlinx.coroutines.flow.firstOrNull 16 | import kotlinx.coroutines.flow.onStart 17 | import kotlinx.coroutines.flow.takeWhile 18 | 19 | class LocationPickerUseCase(private val repository: CartRepository) { 20 | 21 | val query = MutableStateFlow("") 22 | 23 | val locationSearchReducer = ApiReducer>() 24 | val locationReverseReducer = ApiReducer() 25 | 26 | suspend fun searchLocationPlace(query: String, latLon: LatLon) { 27 | if (this.query.value != query) { 28 | this.query.value = query 29 | } 30 | 31 | when { 32 | (query.count() < 3) -> { 33 | this.query.value = "" 34 | } 35 | else -> { 36 | locationSearchReducer 37 | .transform( 38 | transformation = StateTransformation.SimpleTransform(), 39 | call = { 40 | repository.searchLocationPlace(query, latLon) 41 | }, 42 | mapper = { response -> 43 | response.features.orEmpty() 44 | .mapNotNull { it?.toLocationPlace() } 45 | } 46 | ) 47 | } 48 | } 49 | } 50 | 51 | suspend fun getLocationReverse(latLon: LatLon) { 52 | locationReverseReducer.transform( 53 | transformation = StateTransformation.SimpleTransform(), 54 | call = { 55 | repository.getLocationPlace(latLon) 56 | }, 57 | mapper = { 58 | it.toLocationPlace() 59 | } 60 | ) 61 | } 62 | 63 | suspend fun saveShippingAddress(locationPlace: LocationPlace) { 64 | repository.insertShippingLocationPlace(locationPlace) 65 | } 66 | 67 | fun clearData() { 68 | locationSearchReducer.clear() 69 | locationReverseReducer.clear() 70 | query.value = "" 71 | } 72 | 73 | companion object : SingletonCreator() 74 | } -------------------------------------------------------------------------------- /shared/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | kotlin("native.cocoapods") 4 | id("com.android.library") 5 | id("org.jetbrains.compose") 6 | } 7 | 8 | repositories { 9 | google() 10 | mavenCentral() 11 | } 12 | 13 | kotlin { 14 | android() 15 | 16 | iosX64() 17 | iosArm64() 18 | iosSimulatorArm64() 19 | 20 | cocoapods { 21 | version = "1.0.0" 22 | summary = "Some description for the Shared Module" 23 | homepage = "Link to the Shared Module homepage" 24 | ios.deploymentTarget = "14.1" 25 | podfile = project.file("../iosApp/Podfile") 26 | framework { 27 | baseName = "shared" 28 | isStatic = true 29 | } 30 | extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']" 31 | pod("MapboxMaps") { 32 | version = "10.15.0" 33 | moduleName = "shared" 34 | } 35 | } 36 | 37 | sourceSets { 38 | val commonMain by getting { 39 | dependencies { 40 | implementation(project(":libraries:core")) 41 | implementation(project(":libraries:database")) 42 | implementation(project(":libraries:location")) 43 | 44 | implementation(project(":libraries:common")) 45 | implementation(project(":features:home")) 46 | implementation(project(":features:explore")) 47 | implementation(project(":features:wishlist")) 48 | implementation(project(":features:details")) 49 | implementation(project(":features:cart")) 50 | 51 | } 52 | } 53 | val androidMain by getting { 54 | dependencies { 55 | api("com.google.android.gms:play-services-location:21.0.1") 56 | } 57 | } 58 | val iosX64Main by getting 59 | val iosArm64Main by getting 60 | val iosSimulatorArm64Main by getting 61 | val iosMain by creating { 62 | dependsOn(commonMain) 63 | iosX64Main.dependsOn(this) 64 | iosArm64Main.dependsOn(this) 65 | iosSimulatorArm64Main.dependsOn(this) 66 | } 67 | } 68 | } 69 | 70 | android { 71 | compileSdk = (findProperty("android.compileSdk") as String).toInt() 72 | namespace = "com.utsman.tokobola.common" 73 | 74 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 75 | sourceSets["main"].res.srcDirs("src/androidMain/res") 76 | sourceSets["main"].resources.srcDirs("src/commonMain/resources") 77 | 78 | defaultConfig { 79 | minSdk = (findProperty("android.minSdk") as String).toInt() 80 | targetSdk = (findProperty("android.targetSdk") as String).toInt() 81 | } 82 | compileOptions { 83 | sourceCompatibility = JavaVersion.VERSION_11 84 | targetCompatibility = JavaVersion.VERSION_11 85 | } 86 | kotlin { 87 | jvmToolchain(11) 88 | } 89 | } -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/ScaffoldPullRefresh.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.component 2 | 3 | import androidx.compose.animation.core.animateDpAsState 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.BoxScope 6 | import androidx.compose.foundation.layout.BoxWithConstraints 7 | import androidx.compose.foundation.layout.PaddingValues 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.lazy.grid.GridCells 10 | import androidx.compose.foundation.lazy.grid.LazyGridScope 11 | import androidx.compose.foundation.lazy.grid.LazyGridState 12 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 13 | import androidx.compose.material.Scaffold 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.derivedStateOf 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.rememberCoroutineScope 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.Modifier 21 | import androidx.compose.ui.draw.shadow 22 | import androidx.compose.ui.layout.onGloballyPositioned 23 | import androidx.compose.ui.layout.onPlaced 24 | import androidx.compose.ui.unit.Dp 25 | import androidx.compose.ui.unit.IntSize 26 | import androidx.compose.ui.unit.dp 27 | import com.utsman.tokobola.core.utils.pxToDp 28 | import com.utsman.tokobola.core.utils.rememberNavigationBarHeightDp 29 | import kotlinx.coroutines.delay 30 | import kotlinx.coroutines.launch 31 | 32 | @Composable 33 | fun ScaffoldGridState( 34 | modifier: Modifier = Modifier.fillMaxSize(), 35 | lazyGridState: LazyGridState, 36 | fixColumn: Int = 2, 37 | topBarPadding: Dp = Dimens.HeightTopBarSearch, 38 | bottomBarPadding: Dp = (6 + (rememberNavigationBarHeightDp().value * 2)).dp, 39 | topBar: @Composable BoxScope.() -> Unit = {}, 40 | pullRefresh: @Composable BoxScope.() -> Unit = {}, 41 | content: LazyGridScope.() -> Unit 42 | ) { 43 | 44 | val isNeedLift by derivedStateOf { 45 | lazyGridState.canScrollBackward 46 | } 47 | 48 | val elevation by animateDpAsState( 49 | if (isNeedLift) 12.dp else 0.dp 50 | ) 51 | 52 | Scaffold { 53 | BoxWithConstraints { 54 | 55 | } 56 | Box(modifier = modifier) { 57 | LazyVerticalGrid( 58 | columns = GridCells.Fixed(fixColumn), 59 | contentPadding = PaddingValues( 60 | top = topBarPadding + 12.dp, 61 | bottom = bottomBarPadding, 62 | start = 6.dp, 63 | end = 6.dp 64 | ), 65 | state = lazyGridState, 66 | content = content, 67 | ) 68 | 69 | Box( 70 | modifier = Modifier 71 | .shadow(elevation) 72 | ) { 73 | topBar() 74 | } 75 | pullRefresh() 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /features/wishlist/src/commonMain/kotlin/com/utsman/tokobola/wishlist/ui/WishlishCompose.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.wishlist.ui 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.foundation.lazy.grid.GridItemSpan 5 | import androidx.compose.foundation.lazy.grid.items 6 | import androidx.compose.foundation.lazy.grid.rememberLazyGridState 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.collectAsState 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | import com.utsman.tokobola.common.component.Dimens 13 | import com.utsman.tokobola.common.component.EmptyScreen 14 | import com.utsman.tokobola.common.component.ErrorScreen 15 | import com.utsman.tokobola.common.component.ProductItemGrid 16 | import com.utsman.tokobola.common.component.ScaffoldGridState 17 | import com.utsman.tokobola.common.component.SearchBarStaticWithTitle 18 | import com.utsman.tokobola.common.component.Shimmer 19 | import com.utsman.tokobola.core.rememberViewModel 20 | import com.utsman.tokobola.core.utils.onFailure 21 | import com.utsman.tokobola.core.utils.onIdle 22 | import com.utsman.tokobola.core.utils.onLoading 23 | import com.utsman.tokobola.core.utils.onSuccess 24 | import com.utsman.tokobola.wishlist.LocalWishlistUseCase 25 | 26 | @Composable 27 | fun Wishlist() { 28 | val useCase = LocalWishlistUseCase.current 29 | val viewModel = rememberViewModel { WishlistViewModel(useCase) } 30 | 31 | val productState by viewModel.productWishlistState.collectAsState() 32 | 33 | val lazyGridState = rememberLazyGridState() 34 | 35 | ScaffoldGridState( 36 | topBar = { 37 | SearchBarStaticWithTitle( 38 | lazyGridState = lazyGridState, 39 | title = "Your Football Wishlist" 40 | ) 41 | }, 42 | topBarPadding = Dimens.HeightTopBarSearchWithTitle, 43 | lazyGridState = lazyGridState, 44 | modifier = Modifier.fillMaxSize() 45 | ) { 46 | with(productState) { 47 | onIdle { 48 | viewModel.listenProductWishlist() 49 | } 50 | onLoading { 51 | items( 52 | items = listOf(1, 2) 53 | ) { 54 | Shimmer() 55 | } 56 | } 57 | onSuccess { products -> 58 | if (products.isEmpty()) { 59 | item( 60 | span = { GridItemSpan(maxLineSpan) } 61 | ) { 62 | EmptyScreen() 63 | } 64 | } else { 65 | items(products) { product -> 66 | ProductItemGrid(product) 67 | } 68 | } 69 | } 70 | onFailure { 71 | item( 72 | span = { GridItemSpan(maxLineSpan) } 73 | ) { 74 | ErrorScreen(it) 75 | } 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/MapView.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.component 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.collectAsState 6 | import androidx.compose.runtime.derivedStateOf 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.saveable.Saver 10 | import androidx.compose.runtime.saveable.rememberSaveable 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Modifier 13 | import com.utsman.tokobola.core.data.LatLon 14 | import com.utsman.tokobola.core.utils.getOrNull 15 | import com.utsman.tokobola.location.LocalLocationTrackerProvider 16 | import dev.icerock.moko.geo.LatLng 17 | import kotlinx.coroutines.MainScope 18 | import kotlinx.coroutines.delay 19 | import kotlinx.coroutines.flow.firstOrNull 20 | import kotlinx.coroutines.launch 21 | 22 | @Composable 23 | expect fun MapView( 24 | mapConfigState: MapConfigState = rememberMapConfigState(), 25 | modifier: Modifier = Modifier 26 | ) 27 | 28 | data class MapConfig( 29 | val currentLatLon: LatLon = LatLon() 30 | ) 31 | 32 | internal interface MapAction { 33 | fun getCenterLatLon(): LatLon 34 | fun zoomIn() 35 | fun zoomOut() 36 | fun setLocation(latLon: LatLon) 37 | fun addAnnotation(latLon: LatLon, title: String? = null) 38 | } 39 | 40 | fun LatLng.toLatLon(): LatLon { 41 | return LatLon(latitude, longitude) 42 | } 43 | 44 | class MapConfigState(currentLatLon: LatLon) { 45 | var currentLatLon: LatLon by mutableStateOf(currentLatLon) 46 | 47 | internal var mapAction: MapAction? = null 48 | 49 | fun getCenterLocation(): LatLon { 50 | return mapAction?.getCenterLatLon() ?: LatLon() 51 | } 52 | 53 | fun zoomIn() = mapAction?.zoomIn() 54 | fun zoomOut() = mapAction?.zoomOut() 55 | 56 | fun setLocation(latLon: LatLon) { 57 | MainScope().launch { 58 | delay(100) 59 | mapAction?.setLocation(latLon) 60 | } 61 | } 62 | fun addAnnotation(latLon: LatLon, title: String? = null) = mapAction?.addAnnotation(latLon, title) 63 | 64 | companion object { 65 | val Saver: Saver = Saver( 66 | save = { 67 | it.currentLatLon.json() 68 | }, 69 | restore = { 70 | MapConfigState(LatLon.fromJson(it)) 71 | } 72 | ) 73 | } 74 | } 75 | 76 | @Composable 77 | fun rememberMapConfigState(latLon: LatLon? = null): MapConfigState { 78 | val state = if (latLon == null || latLon.isBlank()) { 79 | val trackerProvider = LocalLocationTrackerProvider.current 80 | val location = trackerProvider.getLastKnownLocation() 81 | 82 | LaunchedEffect(Unit) { 83 | trackerProvider.startTracking() 84 | } 85 | 86 | val newLatLon by derivedStateOf { 87 | location 88 | } 89 | 90 | MapConfigState(newLatLon ?: LatLon()) 91 | } else { 92 | MapConfigState(latLon) 93 | } 94 | 95 | 96 | return rememberSaveable(saver = MapConfigState.Saver) { state } 97 | } -------------------------------------------------------------------------------- /features/home/src/commonMain/kotlin/com/utsman/tokobola/home/domain/HomeUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.home.domain 2 | 3 | import com.utsman.tokobola.common.entity.Brand 4 | import com.utsman.tokobola.common.entity.HomeBanner 5 | import com.utsman.tokobola.common.entity.ThumbnailProduct 6 | import com.utsman.tokobola.common.toBrand 7 | import com.utsman.tokobola.common.toHomeBanner 8 | import com.utsman.tokobola.common.toThumbnailProduct 9 | import com.utsman.tokobola.core.SingletonCreator 10 | import com.utsman.tokobola.core.data.Paged 11 | import com.utsman.tokobola.network.ApiReducer 12 | import com.utsman.tokobola.network.AutoPagingAdapter 13 | import com.utsman.tokobola.network.StateTransformation 14 | 15 | class HomeUseCase(private val homeRepository: HomeRepository) { 16 | 17 | val productsFeaturedReducer = ApiReducer>() 18 | val productBannerReducer = ApiReducer>() 19 | val brandReducer = ApiReducer>() 20 | 21 | val productViewedReducer = ApiReducer>() 22 | 23 | private val featurePagingAdapter = AutoPagingAdapter(productsFeaturedReducer) 24 | 25 | suspend fun getProduct() { 26 | featurePagingAdapter.executeResponse( 27 | call = { 28 | homeRepository.getFeaturedProductPaged(it) 29 | }, 30 | mapper = { 31 | it.toThumbnailProduct() 32 | } 33 | ) 34 | } 35 | 36 | suspend fun getBanner() { 37 | productBannerReducer.transform( 38 | call = { 39 | homeRepository.getBanner() 40 | }, 41 | mapper = { bannerResponse -> 42 | bannerResponse.data?.map { it.toHomeBanner() }.orEmpty() 43 | } 44 | ) 45 | } 46 | 47 | suspend fun getBrand() { 48 | brandReducer.transform( 49 | call = { 50 | homeRepository.getBrand() 51 | }, 52 | mapper = { brandResponse -> 53 | brandResponse.data?.map { it.toBrand() }.orEmpty() 54 | .filter { 55 | // filter "other" brand 56 | it.id != 40 57 | } 58 | } 59 | ) 60 | } 61 | 62 | suspend fun getAllProductViewed() { 63 | homeRepository.getAllRecentlyViewedFlow() 64 | .collect { realms -> 65 | productViewedReducer.transform( 66 | transformation = StateTransformation.SimpleTransform(), 67 | call = { 68 | val ids = realms.map { it.productId } 69 | homeRepository.getThumbnailByIds(ids) 70 | }, 71 | mapper = { thumbnailProductResponse -> 72 | thumbnailProductResponse.data 73 | ?.map { it.toThumbnailProduct() } 74 | .orEmpty() 75 | } 76 | ) 77 | } 78 | } 79 | 80 | fun clearProductPage() { 81 | featurePagingAdapter.clear() 82 | brandReducer.clear() 83 | productViewedReducer.clear() 84 | } 85 | 86 | companion object : SingletonCreator() 87 | } -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/TopBar.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.component 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.interaction.MutableInteractionSource 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.foundation.layout.wrapContentHeight 12 | import androidx.compose.foundation.lazy.grid.LazyGridState 13 | import androidx.compose.foundation.shape.CircleShape 14 | import androidx.compose.material.MaterialTheme 15 | import androidx.compose.material.Text 16 | import androidx.compose.material.ripple.rememberRipple 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.draw.clip 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.graphics.ColorFilter 25 | import androidx.compose.ui.text.font.FontWeight 26 | import androidx.compose.ui.unit.dp 27 | import com.utsman.tokobola.core.navigation.LocalNavigation 28 | import com.utsman.tokobola.core.utils.rememberStatusBarHeightDp 29 | import com.utsman.tokobola.resources.SharedRes 30 | import dev.icerock.moko.resources.compose.painterResource 31 | 32 | @Composable 33 | fun TopBar(text: String, modifier: Modifier = Modifier, lazyGridState: LazyGridState) { 34 | val navigation = LocalNavigation.current 35 | val titleColor by lazyGridState.animatedColor( 36 | from = Color.White, 37 | to = MaterialTheme.colors.primary 38 | ) 39 | 40 | val topBarColor by lazyGridState.animatedTopBarColor 41 | 42 | Row( 43 | modifier = modifier 44 | .background(color = topBarColor) 45 | .wrapContentHeight() 46 | .padding( 47 | top = 12.dp + rememberStatusBarHeightDp(), 48 | start = 12.dp, 49 | end = 12.dp, 50 | bottom = 12.dp 51 | ), 52 | horizontalArrangement = Arrangement.Center, 53 | verticalAlignment = Alignment.CenterVertically 54 | ) { 55 | Image( 56 | modifier = Modifier.size(34.dp) 57 | .clickable( 58 | interactionSource = remember { MutableInteractionSource() }, 59 | indication = rememberRipple(false), 60 | onClick = { 61 | navigation.back() 62 | }) 63 | .clip(CircleShape) 64 | .background( 65 | color = MaterialTheme.colors.secondary.copy(alpha = 0.6f) 66 | ) 67 | .padding(6.dp), 68 | painter = painterResource(SharedRes.images.arrow_back_default), 69 | contentDescription = "", 70 | colorFilter = ColorFilter.tint(Color.White) 71 | ) 72 | 73 | Text( 74 | text = text, 75 | fontWeight = FontWeight.Bold, 76 | modifier = Modifier.weight(1f).padding(12.dp), 77 | color = titleColor 78 | ) 79 | } 80 | } -------------------------------------------------------------------------------- /libraries/common/src/commonMain/kotlin/com/utsman/tokobola/common/component/ErrorScreen.kt: -------------------------------------------------------------------------------- 1 | package com.utsman.tokobola.common.component 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.foundation.layout.size 11 | import androidx.compose.material.MaterialTheme 12 | import androidx.compose.material.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.text.font.FontWeight 18 | import androidx.compose.ui.text.style.TextAlign 19 | import androidx.compose.ui.unit.dp 20 | import com.utsman.tokobola.resources.SharedRes 21 | import dev.icerock.moko.resources.compose.painterResource 22 | 23 | @Composable 24 | fun ErrorScreen(throwable: Throwable) { 25 | Column( 26 | modifier = Modifier.fillMaxSize(), 27 | horizontalAlignment = Alignment.CenterHorizontally, 28 | verticalArrangement = Arrangement.Center 29 | ) { 30 | val painter = painterResource(SharedRes.images.image_error) 31 | Image( 32 | painter = painter, 33 | contentDescription = "", 34 | modifier = Modifier.size(100.dp) 35 | ) 36 | Text( 37 | text = throwable.message.orEmpty(), 38 | color = Color.Red.copy(alpha = 0.7f), 39 | fontWeight = FontWeight.Black, 40 | textAlign = TextAlign.Center, 41 | modifier = Modifier.fillMaxWidth() 42 | .padding(22.dp) 43 | ) 44 | } 45 | } 46 | 47 | @Composable 48 | fun SimpleErrorScreen(throwable: Throwable) { 49 | Row( 50 | modifier = Modifier.fillMaxSize() 51 | .padding(12.dp), 52 | verticalAlignment = Alignment.CenterVertically 53 | ) { 54 | val painter = painterResource(SharedRes.images.icon_emoji_error) 55 | Image( 56 | painter = painter, 57 | contentDescription = "", 58 | modifier = Modifier.size(24.dp) 59 | ) 60 | Text( 61 | text = throwable.message.orEmpty(), 62 | color = Color.Red.copy(alpha = 0.7f), 63 | fontWeight = FontWeight.Black, 64 | textAlign = TextAlign.Center, 65 | modifier = Modifier.padding(22.dp) 66 | ) 67 | } 68 | } 69 | 70 | @Composable 71 | fun EmptyScreen(message: String = "Nothing") { 72 | Column( 73 | modifier = Modifier.fillMaxSize(), 74 | horizontalAlignment = Alignment.CenterHorizontally, 75 | verticalArrangement = Arrangement.Center 76 | ) { 77 | val painter = painterResource(SharedRes.images.image_empty) 78 | Image( 79 | painter = painter, 80 | contentDescription = "", 81 | modifier = Modifier.size(180.dp) 82 | ) 83 | Text( 84 | text = message, 85 | color = MaterialTheme.colors.primary, 86 | fontWeight = FontWeight.Black, 87 | textAlign = TextAlign.Center, 88 | modifier = Modifier.fillMaxWidth() 89 | .padding(22.dp) 90 | ) 91 | } 92 | } --------------------------------------------------------------------------------