diff --git a/.run/spotlessApply.run.xml b/.run/spotlessApply.run.xml index 5c4dac83..5be23fba 100644 --- a/.run/spotlessApply.run.xml +++ b/.run/spotlessApply.run.xml @@ -18,6 +18,29 @@ true true false + false + + + + + + + + true + true + false + false \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9ed5d5ce..ef1ec016 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -118,6 +118,7 @@ dependencies { implementation(project(":exoplayer")) implementation(project(":cms")) implementation(project(":popbackstack")) + implementation(project(":store")) "baselineProfile"(project(":benchmarks")) implementation(libs.kotlin.stdlib) diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/NavGraphMain.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/NavGraphMain.kt index 6c5aae43..940b4aeb 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/NavGraphMain.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/NavGraphMain.kt @@ -50,6 +50,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument import androidx.navigation.navigation +import com.example.store.ui.screen.StoreMainScreen import org.imaginativeworld.whynotcompose.base.extensions.getJsonFromObj import org.imaginativeworld.whynotcompose.base.extensions.getObjFromJson import org.imaginativeworld.whynotcompose.base.extensions.navArg @@ -239,6 +240,7 @@ sealed class TutorialsScreen(val route: String) { data object TutorialTicTacToe : TutorialsScreen("tutorial/tic-tac-toe") data object TutorialExoPlayer : TutorialsScreen("tutorial/exoplayer") data object TutorialCMS : TutorialsScreen("tutorial/cms") + data object TutorialStore : TutorialsScreen("tutorial/store") data object TutorialDeepLink : TutorialsScreen("tutorial/deep-link") // ================================================================ @@ -982,6 +984,15 @@ private fun NavGraphBuilder.addTutorialIndexScreen( ) } + composable(TutorialsScreen.TutorialStore.route) { + StoreMainScreen( + updateUiThemeMode = updateUiThemeMode, + goBack = { + navController.popBackStackOrIgnore() + } + ) + } + composable(TutorialsScreen.TutorialDeepLink.route) { DeepLinksScreen( goBack = { diff --git a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/index/TutorialList.kt b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/index/TutorialList.kt index 0d969788..8ee29648 100644 --- a/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/index/TutorialList.kt +++ b/app/src/main/kotlin/org/imaginativeworld/whynotcompose/ui/screens/tutorial/index/TutorialList.kt @@ -129,6 +129,12 @@ data class Tutorial( route = TutorialsScreen.TutorialCMS, level = TutorialLevel.Advanced ), + Tutorial( + name = "Store", + description = "Example of a Content Management System.", + route = TutorialsScreen.TutorialStore, + level = TutorialLevel.Advanced + ), Tutorial( name = "Deep Link", description = "Example of Deep Link.", diff --git a/app/src/main/res/drawable/store.jpg b/app/src/main/res/drawable/store.jpg new file mode 100644 index 00000000..2ada3e6b Binary files /dev/null and b/app/src/main/res/drawable/store.jpg differ diff --git a/base/src/main/kotlin/org/imaginativeworld/whynotcompose/base/utils/Constants.kt b/base/src/main/kotlin/org/imaginativeworld/whynotcompose/base/utils/Constants.kt index 53263537..bd48c2b6 100644 --- a/base/src/main/kotlin/org/imaginativeworld/whynotcompose/base/utils/Constants.kt +++ b/base/src/main/kotlin/org/imaginativeworld/whynotcompose/base/utils/Constants.kt @@ -34,6 +34,7 @@ object Constants { */ const val SERVER_ENDPOINT = "https://imaginativeworld.org" const val CMS_SERVER_ENDPOINT = "https://gorest.co.in/public" + const val STORE_SERVER_ENDPOINT = "https://fakestoreapi.com" /** * For MyNotificationOpenedHandler diff --git a/common-ui-compose/src/main/res/drawable/store.jpg b/common-ui-compose/src/main/res/drawable/store.jpg new file mode 100644 index 00000000..2ada3e6b Binary files /dev/null and b/common-ui-compose/src/main/res/drawable/store.jpg differ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 15f329a0..4047bae0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -104,6 +104,7 @@ swiperefreshlayout = "1.1.0" mlkitBarcodeScanning = "17.3.0" cameraX = "1.4.1" haze = "1.2.2" +material = "1.8.0" [libraries] @@ -236,6 +237,10 @@ mlkit-barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning" haze = { group = "dev.chrisbanes.haze", name = "haze", version.ref = "haze" } haze-materials = { group = "dev.chrisbanes.haze", name = "haze-materials", version.ref = "haze" } +androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } + + # The following line is optional, as the core library is included indirectly by camera-camera2 camerax-core = { group = "androidx.camera", name = "camera-core", version.ref = "cameraX" } camerax-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraX" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 57fdea9b..6e877b70 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -56,3 +56,4 @@ include(":exoplayer") include(":cms") include(":popbackstack") include(":benchmarks") +include(":store") diff --git a/store/.gitignore b/store/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/store/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/store/build.gradle.kts b/store/build.gradle.kts new file mode 100644 index 00000000..de7bea4d --- /dev/null +++ b/store/build.gradle.kts @@ -0,0 +1,137 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose) + alias(libs.plugins.ksp) +} + +android { + namespace = "org.imaginativeworld.whynotcompose.store" + compileSdk = BuildConfigConst.compileSdk + + defaultConfig { + minSdk = BuildConfigConst.minSdk + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + + freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn" + + // Enable experimental compose APIs + freeCompilerArgs = + freeCompilerArgs + "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api" + freeCompilerArgs = + freeCompilerArgs + "-opt-in=androidx.compose.animation.ExperimentalAnimationApi" + freeCompilerArgs = + freeCompilerArgs + "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi" + freeCompilerArgs = + freeCompilerArgs + "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi" + } + + ksp { + arg("room.schemaLocation", "$projectDir/schemas") + } + + buildFeatures { + buildConfig = true + compose = true + } +} + +dependencies { + implementation(project(":base")) + implementation(project(":common-ui-compose")) + + implementation(libs.kotlin.stdlib) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.swiperefreshlayout) + + // ---------------------------------------------------------------- + // Compose + // ---------------------------------------------------------------- + implementation(platform(libs.androidx.compose.bom)) + + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.util) + // Tooling support (Previews, etc.) + debugImplementation(libs.androidx.compose.ui.tooling) + implementation(libs.androidx.compose.ui.tooling.preview) + // Animation + implementation(libs.androidx.compose.animation) + // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.foundation.layout) + // Material Design + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material3.windowSizeClass) + // Material design icons + implementation(libs.androidx.compose.material.iconsCore) + implementation(libs.androidx.compose.material.iconsExtended) + // Integration with observables + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.runtime.livedata) + implementation(libs.androidx.compose.runtime.tracing) + // compose Navigation Component + implementation(libs.androidx.navigation.compose) + // Constraint Layout + implementation(libs.androidx.constraintlayout.compose) + // Integration with activities + implementation(libs.androidx.activity.compose) + + // Jetpack compose Integration for ViewModel + implementation(libs.androidx.lifecycle.viewmodel.compose) + + // Paging + implementation(libs.androidx.paging.compose) + + // ---------------------------------------------------------------- + + // Timber + implementation(libs.timber) + + // Hilt + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + implementation(libs.androidx.hilt.navigation.compose) + + // Retrofit + implementation(libs.retrofit.core) + implementation(libs.okhttp.logging) + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + + // Moshi + implementation(libs.retrofit.moshi) + implementation(libs.moshi.kotlin) + ksp(libs.moshi.kotlin.codegen) + + // Room Persistence Library + implementation(libs.room.runtime) + ksp(libs.room.compiler) + + // Room: Kotlin Extensions and Coroutines support for Room + implementation(libs.room.ktx) + + // Coil + implementation(libs.coil.compose) + implementation(libs.coil.svg) + implementation(libs.coil.network) + + // Serialization + implementation(libs.kotlinx.serialization.json) + + // Haze + implementation(libs.haze) + implementation(libs.haze.materials) + + implementation(libs.androidx.material3) + implementation(libs.material) +} diff --git a/store/src/main/AndroidManifest.xml b/store/src/main/AndroidManifest.xml new file mode 100644 index 00000000..44008a43 --- /dev/null +++ b/store/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/store/src/main/kotlin/com/example/store/datasource/ProductPagingSource.kt b/store/src/main/kotlin/com/example/store/datasource/ProductPagingSource.kt new file mode 100644 index 00000000..0afb0c35 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/datasource/ProductPagingSource.kt @@ -0,0 +1,62 @@ +package com.example.store.datasource + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.example.store.models.product.Product +import com.example.store.repositories.ProductRepository +import okio.IOException +import org.imaginativeworld.whynotcompose.base.network.ApiException +import retrofit2.HttpException + +class ProductPagingSource( + private val repository: ProductRepository +) : PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult { + val pagePosition = params.key ?: 1 + + return try { + val products = repository.getProducts( + pagePosition + ) + + if (products == null) { + LoadResult.Error(ApiException("No data returned!")) + } else { + val nextKey = if (products.isEmpty()) { + null + } else { + pagePosition + 1 + } + + LoadResult.Page( + data = products, + prevKey = if (pagePosition == 1L) null else pagePosition - 1, + nextKey = nextKey + ) + } + } catch (exception: IOException) { + return LoadResult.Error(exception) + } catch (exception: HttpException) { + return LoadResult.Error(exception) + } catch (exception: ApiException) { + return LoadResult.Error(exception) + } + } + + // The refresh key is used for subsequent refresh calls to PagingSource.load after the initial load + override fun getRefreshKey(state: PagingState): Long? { + // We need to get the previous key (or next key if previous is null) of the page + // that was closest to the most recently accessed index. + // Anchor position is the most recently accessed index + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + + // For cursor paging use this: + // https://stackoverflow.com/questions/67691903/how-to-implement-pagingsource-getrefreshkey-for-cursor-based-pagination-androi + // val anchorPageIndex = state.pages.indexOf(state.closestPageToPosition(anchorPosition)) + // state.pages.getOrNull(anchorPageIndex + 1)?.prevKey ?: state.pages.getOrNull(anchorPageIndex - 1)?.nextKey + } + } +} diff --git a/store/src/main/kotlin/com/example/store/di/StoreAppModule.kt b/store/src/main/kotlin/com/example/store/di/StoreAppModule.kt new file mode 100644 index 00000000..87d414f3 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/di/StoreAppModule.kt @@ -0,0 +1,40 @@ +package com.example.store.di + +import com.example.store.network.api.CategoriesApiInterface +import com.example.store.network.api.ProductDetailsApiInterface +import com.example.store.network.api.ProductsApiInterface +import com.squareup.moshi.Moshi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Named +import javax.inject.Singleton +import org.imaginativeworld.whynotcompose.base.network.ApiClient +import org.imaginativeworld.whynotcompose.base.utils.Constants +import retrofit2.Retrofit + +@InstallIn(SingletonComponent::class) +@Module +class StoreAppModule { + @Singleton + @Provides + @Named("STORE") + fun provideRetrofit(moshi: Moshi): Retrofit = ApiClient.getRetrofit( + moshi, + Constants.STORE_SERVER_ENDPOINT + "/", + mapOf() + ) + + @Singleton + @Provides + fun provideProductApiInterface(@Named("STORE") retrofit: Retrofit): ProductsApiInterface = retrofit.create(ProductsApiInterface::class.java) + + @Singleton + @Provides + fun provideProductDetailsApiInterface(@Named("STORE") retrofit: Retrofit): ProductDetailsApiInterface = retrofit.create(ProductDetailsApiInterface::class.java) + + @Singleton + @Provides + fun provideCategoriesApiInterface(@Named("STORE") retrofit: Retrofit): CategoriesApiInterface = retrofit.create(CategoriesApiInterface::class.java) +} diff --git a/store/src/main/kotlin/com/example/store/models/categorie/Categories.kt b/store/src/main/kotlin/com/example/store/models/categorie/Categories.kt new file mode 100644 index 00000000..06314cb2 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/models/categorie/Categories.kt @@ -0,0 +1,3 @@ +package com.example.store.models.categorie + +class Categories : ArrayList() diff --git a/store/src/main/kotlin/com/example/store/models/categorie/Category.kt b/store/src/main/kotlin/com/example/store/models/categorie/Category.kt new file mode 100644 index 00000000..aa69a27c --- /dev/null +++ b/store/src/main/kotlin/com/example/store/models/categorie/Category.kt @@ -0,0 +1,14 @@ +package com.example.store.models.categorie + +data class Category( + val id: Int, + val name: String, + val imageUrl: String +) + +val categories = listOf( + Category(id = 1, name = "Electron-ics", imageUrl = "https://fakestoreapi.com/img/61IBBVJvSDL._AC_SY879_.jpg"), + Category(id = 2, name = "Jewelery", imageUrl = "https://fakestoreapi.com/img/71pWzhdJNwL._AC_UL640_QL65_ML3_.jpg"), + Category(id = 3, name = "Men's Clothing", imageUrl = "https://fakestoreapi.com/img/71pWzhdJNwL._AC_UL640_QL65_ML3_.jpg"), + Category(id = 4, name = "Women's Clothing", imageUrl = "https://fakestoreapi.com/img/61IBBVJvSDL._AC_SY879_.jpg") +) diff --git a/store/src/main/kotlin/com/example/store/models/product/Product.kt b/store/src/main/kotlin/com/example/store/models/product/Product.kt new file mode 100644 index 00000000..92162aac --- /dev/null +++ b/store/src/main/kotlin/com/example/store/models/product/Product.kt @@ -0,0 +1,24 @@ +package com.example.store.models.product + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Product( + @Json(name = "category") + val category: String, + @Json(name = "description") + val description: String, + @Json(name = "id") + val id: Int, + @Json(name = "image") + val image: String, + @Json(name = "price") + val price: Double, + @Json(name = "rating") + val rating: Rating, + @Json(name = "title") + val title: String, + @Json(name = "quantity") + var quantity: Int? = null +) diff --git a/store/src/main/kotlin/com/example/store/models/product/ProductsResponse.kt b/store/src/main/kotlin/com/example/store/models/product/ProductsResponse.kt new file mode 100644 index 00000000..23f244af --- /dev/null +++ b/store/src/main/kotlin/com/example/store/models/product/ProductsResponse.kt @@ -0,0 +1,3 @@ +package com.example.store.models.product + +class ProductsResponse : ArrayList() \ No newline at end of file diff --git a/store/src/main/kotlin/com/example/store/models/product/Rating.kt b/store/src/main/kotlin/com/example/store/models/product/Rating.kt new file mode 100644 index 00000000..a2bf6c2b --- /dev/null +++ b/store/src/main/kotlin/com/example/store/models/product/Rating.kt @@ -0,0 +1,12 @@ +package com.example.store.models.product + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Rating( + @Json(name = "count") + val count: Int, + @Json(name = "rate") + val rate: Double +) diff --git a/store/src/main/kotlin/com/example/store/network/api/CategoriesApiInterface.kt b/store/src/main/kotlin/com/example/store/network/api/CategoriesApiInterface.kt new file mode 100644 index 00000000..37c9a881 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/network/api/CategoriesApiInterface.kt @@ -0,0 +1,10 @@ +package com.example.store.network.api + +import retrofit2.Response +import retrofit2.http.GET + +interface CategoriesApiInterface { + + @GET("products/categories") + suspend fun getCategories(): Response> +} diff --git a/store/src/main/kotlin/com/example/store/network/api/ProductDetailsApiInterface.kt b/store/src/main/kotlin/com/example/store/network/api/ProductDetailsApiInterface.kt new file mode 100644 index 00000000..4f1fab5b --- /dev/null +++ b/store/src/main/kotlin/com/example/store/network/api/ProductDetailsApiInterface.kt @@ -0,0 +1,12 @@ +package com.example.store.network.api + +import com.example.store.models.product.Product +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Path + +interface ProductDetailsApiInterface { + + @GET("products/{id}") + suspend fun getProductDetails(@Path("id") id: Int): Response +} diff --git a/store/src/main/kotlin/com/example/store/network/api/ProductsApiInterface.kt b/store/src/main/kotlin/com/example/store/network/api/ProductsApiInterface.kt new file mode 100644 index 00000000..9f5518eb --- /dev/null +++ b/store/src/main/kotlin/com/example/store/network/api/ProductsApiInterface.kt @@ -0,0 +1,12 @@ +package com.example.store.network.api + +import com.example.store.models.product.Product +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Query + +interface ProductsApiInterface { + + @GET("products") + suspend fun getProducts(@Query("page") page: Long): Response> +} diff --git a/store/src/main/kotlin/com/example/store/repositories/CategoriesRepository.kt b/store/src/main/kotlin/com/example/store/repositories/CategoriesRepository.kt new file mode 100644 index 00000000..ef4a8ade --- /dev/null +++ b/store/src/main/kotlin/com/example/store/repositories/CategoriesRepository.kt @@ -0,0 +1,20 @@ +package com.example.store.repositories + +import android.content.Context +import com.example.store.network.api.CategoriesApiInterface +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.imaginativeworld.whynotcompose.base.network.SafeApiRequest + +class CategoriesRepository @Inject constructor( + @ApplicationContext private val context: Context, + private val categoriesApi: CategoriesApiInterface +) { + suspend fun getCategories() = withContext(Dispatchers.IO) { + SafeApiRequest.apiRequest(context) { + categoriesApi.getCategories() + } + } +} diff --git a/store/src/main/kotlin/com/example/store/repositories/ProductRepository.kt b/store/src/main/kotlin/com/example/store/repositories/ProductRepository.kt new file mode 100644 index 00000000..1558699f --- /dev/null +++ b/store/src/main/kotlin/com/example/store/repositories/ProductRepository.kt @@ -0,0 +1,30 @@ +package com.example.store.repositories + +import android.content.Context +import com.example.store.network.api.ProductDetailsApiInterface +import com.example.store.network.api.ProductsApiInterface +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.imaginativeworld.whynotcompose.base.network.SafeApiRequest + +class ProductRepository @Inject constructor( + @ApplicationContext private val context: Context, + private val productApi: ProductsApiInterface, + + private val productDetailsApi: ProductDetailsApiInterface + +) { + suspend fun getProducts(page: Long) = withContext(Dispatchers.IO) { + SafeApiRequest.apiRequest(context) { + productApi.getProducts(page) + } + } + + suspend fun getProductsId(id: Int) = withContext(Dispatchers.IO) { + SafeApiRequest.apiRequest(context) { + productDetailsApi.getProductDetails(id) + } + } +} diff --git a/store/src/main/kotlin/com/example/store/theme/Color.kt b/store/src/main/kotlin/com/example/store/theme/Color.kt new file mode 100644 index 00000000..b46315b1 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/theme/Color.kt @@ -0,0 +1,11 @@ +package com.example.store.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) diff --git a/store/src/main/kotlin/com/example/store/theme/Theme.kt b/store/src/main/kotlin/com/example/store/theme/Theme.kt new file mode 100644 index 00000000..6fcb4c75 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/theme/Theme.kt @@ -0,0 +1,29 @@ +package com.example.store.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +private val DarkColorPalette = darkColorScheme() + +private val LightColorPalette = lightColorScheme() + +@Composable +fun StoreAppTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme( + colorScheme = colors, + typography = Typography, + content = content + ) +} diff --git a/store/src/main/kotlin/com/example/store/theme/Type.kt b/store/src/main/kotlin/com/example/store/theme/Type.kt new file mode 100644 index 00000000..50801834 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/theme/Type.kt @@ -0,0 +1,34 @@ +package com.example.store.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) diff --git a/store/src/main/kotlin/com/example/store/ui/compositions/CartCard.kt b/store/src/main/kotlin/com/example/store/ui/compositions/CartCard.kt new file mode 100644 index 00000000..a04dd4d0 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/compositions/CartCard.kt @@ -0,0 +1,180 @@ +package com.example.store.ui.compositions + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.example.store.theme.StoreAppTheme +import com.example.store.ui.screen.productdetails.Product +import com.example.store.ui.screen.productdetails.dummyProducts + +@Composable +fun CartItemCard( + product: Product, + onQuantityChange: (Int) -> Unit, + modifier: Modifier = Modifier +) { + var quantity by remember { mutableIntStateOf(2) } + val totalPrice = product.price * quantity + + Card( + modifier = modifier + .fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AsyncImage( + model = product.imageUrl, + contentDescription = null, + modifier = Modifier + .width(80.dp) + .height(80.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color.White) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Column( + modifier = Modifier + .weight(1f) + ) { + Text( + product.title, + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + fontWeight = FontWeight.Bold + ) + Text( + text = "$${product.price}", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold + ) + } + } + + HorizontalDivider() + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.weight(.4f) + ) { + IconButton( + onClick = { + if (quantity > 1) { + quantity-- + onQuantityChange(quantity) + } + } + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = .7f)) + .padding(4.dp), +// .border(1.dp, MaterialTheme.colorScheme.surface), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.Remove, + contentDescription = "Decrease", + tint = MaterialTheme.colorScheme.surface + ) + } + } + + Text( + text = "$quantity", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 8.dp) + ) + + IconButton( + onClick = { + quantity++ + onQuantityChange(quantity) + } + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = .7f)) + .padding(4.dp) + ) { + Icon( + Icons.Default.Add, + contentDescription = "Increase", + tint = MaterialTheme.colorScheme.surface + ) + } + } + } + + Text( + text = "$$totalPrice", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.End, + color = MaterialTheme.colorScheme.error, + modifier = Modifier + .padding(horizontal = 8.dp) + .weight(.5f) + ) + } + } +} + +@PreviewLightDark +@Composable +private fun CartItemCardPreview() { + StoreAppTheme { + CartItemCard( + product = dummyProducts[0], + onQuantityChange = {}, + modifier = Modifier + ) + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/compositions/CategoryCard.kt b/store/src/main/kotlin/com/example/store/ui/compositions/CategoryCard.kt new file mode 100644 index 00000000..90e6f16d --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/compositions/CategoryCard.kt @@ -0,0 +1,90 @@ +package com.example.store.ui.compositions + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import com.example.store.models.categorie.Category +import com.example.store.theme.StoreAppTheme +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.materials.HazeMaterials + +@Composable +fun CategoryCard( + category: Category, + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + val hazeState = remember { HazeState() } + Box( + modifier = modifier + .size(150.dp) + .clip(RoundedCornerShape(12.dp)) + .hazeSource(state = hazeState) + .clickable { + onClick() + } + ) { + Image( + painter = painterResource(id = org.imaginativeworld.whynotcompose.common.compose.R.drawable.store), + contentDescription = "", + contentScale = ContentScale.Crop, + modifier = Modifier.matchParentSize() + ) + + Box( + modifier = Modifier + .wrapContentSize() + .align(Alignment.Center) + .padding(16.dp) + .clip(RoundedCornerShape(20)) + .background(MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.9f)) + .hazeEffect(state = hazeState, style = HazeMaterials.ultraThin()), + contentAlignment = Alignment.Center + ) { + Text( + text = category.name, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.ExtraBold, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.surface, + modifier = Modifier + .padding(12.dp) + .align(Alignment.Center) + ) + } + } +} + +@PreviewLightDark +@Composable +private fun CategoryCardPreview() { + StoreAppTheme { + CategoryCard( + category = Category( + id = 1, + name = " Women's Clothing", + imageUrl = "" + ) + ) + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/compositions/KeyValue.kt b/store/src/main/kotlin/com/example/store/ui/compositions/KeyValue.kt new file mode 100644 index 00000000..e8e89da4 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/compositions/KeyValue.kt @@ -0,0 +1,36 @@ +package com.example.store.ui.compositions + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun KeyValue( + title: String, + value: String +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + } +} \ No newline at end of file diff --git a/store/src/main/kotlin/com/example/store/ui/compositions/OrderCard.kt b/store/src/main/kotlin/com/example/store/ui/compositions/OrderCard.kt new file mode 100644 index 00000000..58895f00 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/compositions/OrderCard.kt @@ -0,0 +1,136 @@ +package com.example.store.ui.compositions + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import com.example.store.theme.StoreAppTheme +import com.example.store.ui.screen.order.Order +import com.example.store.ui.screen.productdetails.Product + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun OrderCard( + order: Order, + modifier: Modifier = Modifier +) { + var isExpanded by remember { mutableStateOf(true) } + + Column( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.onBackground) + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "#${order.id}", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.surface + ) + Text( + text = order.date, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.surface + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = { isExpanded = !isExpanded }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ), + shape = MaterialTheme.shapes.medium + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Total ${order.products.size} ${ + if (order.products.size > 1) { + "products" + } else { + "product" + } + }", + color = MaterialTheme.colorScheme.primary + ) + Icon( + imageVector = if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, + contentDescription = "Expand", + tint = MaterialTheme.colorScheme.primary + ) + } + } + + if (isExpanded) { + Spacer(modifier = Modifier.height(8.dp)) + Column { + order.products.forEach { product -> + OrderProductItem(product = product) + Spacer(modifier = Modifier.height(4.dp)) + } + } + } + } +} + +@PreviewLightDark +@Composable +private fun OrderCardPreview() { + StoreAppTheme { + OrderCard( + order = Order( + id = 7, + date = "01 Mar 2020", + products = listOf( + Product( + category = "Backpack", + id = 1, + title = "Fits 15 Laptops", + imageUrl = "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg", + price = 109.95, + rating = 4.5, + reviewCount = 120, + description = "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday", + quantity = 10 + ) + ) + ) + ) + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/compositions/OrderProductItem.kt b/store/src/main/kotlin/com/example/store/ui/compositions/OrderProductItem.kt new file mode 100644 index 00000000..7798b680 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/compositions/OrderProductItem.kt @@ -0,0 +1,107 @@ +package com.example.store.ui.compositions + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import com.example.store.theme.StoreAppTheme +import com.example.store.ui.screen.productdetails.Product + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun OrderProductItem( + product: Product, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = org.imaginativeworld.whynotcompose.common.compose.R.drawable.store), + contentDescription = "", + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(8.dp)), + contentScale = ContentScale.Crop + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = product.title, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + color = MaterialTheme.colorScheme.outline, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() + ) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = buildAnnotatedString { + append("$${product.price} × ") + withStyle(SpanStyle(color = MaterialTheme.colorScheme.error)) { + append("${product.quantity}") + } + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline, + ) + + Text( + text = "$${"%.2f".format(product.price * product.quantity)}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.Bold, + ) + } + } + } +} + +@PreviewLightDark +@Composable +private fun OrderProductItemPreview() { + StoreAppTheme { + OrderProductItem( + product = Product( + category = "Jacket", + id = 3, + title = "Mens Cotton Jacket", + imageUrl = "https://fakestoreapi.com/img/71li-ujtlUL._AC_UX679_.jpg", + price = 55.99, + rating = 4.5, + reviewCount = 500, + description = "Great outerwear jackets for Spring/Autumn/Winter, suitable for many occasions, such as working, hiking, camping, mountain/rock climbing, cycling.", + quantity = 5 + ) + ) + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/compositions/ProductItem.kt b/store/src/main/kotlin/com/example/store/ui/compositions/ProductItem.kt new file mode 100644 index 00000000..e8a7488d --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/compositions/ProductItem.kt @@ -0,0 +1,197 @@ +package com.example.store.ui.compositions + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.example.store.models.product.Product +import com.example.store.theme.StoreAppTheme + +@Composable +fun ProductItem( + product: Product, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + var quantity by remember { mutableIntStateOf(0) } + + Card( + modifier = modifier + .clickable { onClick() }, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ), + elevation = CardDefaults.cardElevation(4.dp), + shape = RoundedCornerShape(8.dp) + ) { + Column( + modifier = Modifier + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + AsyncImage( + model = product.image, + contentDescription = "Product Image", + modifier = Modifier + .width(150.dp) + .height(150.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color.White) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = product.title, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Start, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + ) + Text( + text = "$${product.price}", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Start, + fontWeight = FontWeight.Bold, + modifier = Modifier.fillMaxWidth() + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + Icons.Default.Star, + contentDescription = "Rating", + tint = Color.Yellow.copy(alpha = .5f) + ) + Text( + text = ("${product.rating.rate} (${product.rating.count})"), + modifier = Modifier.padding(start = 4.dp), + color = MaterialTheme.colorScheme.outline + ) + } + } + + HorizontalDivider() + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + IconButton( + onClick = { if (quantity > 0) quantity-- }, + modifier = Modifier + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = .7f)) + .padding(4.dp), +// .border(1.dp, MaterialTheme.colorScheme.surface), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.Remove, + contentDescription = "Decrease", + tint = MaterialTheme.colorScheme.surface + ) + } + } + + Text( + text = "$quantity", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 8.dp) + ) + + IconButton( + onClick = { quantity++ } + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.primary.copy(alpha = .7f)) + .padding(4.dp), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.Add, + contentDescription = "Increase", + tint = MaterialTheme.colorScheme.surface + ) + } + } + } + } +} + +@PreviewLightDark +@Composable +private fun PreviewProductDetailsScreen() { + StoreAppTheme { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { +// ProductItem( +// onClick = {}, +// product = Product( +// category = "jewelery", +// id = 1, +// title = "WD 2TB Elements Portable External...", +// imageUrl = "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg", +// price = 64.00, +// rating = 3.3, +// reviewCount = 203, +// description = "USB 3.0 and USB 2.0 Compatibility Fast data transfers Improve PC Performance High Capacity;...", +// quantity = 0 +// ) +// ) + } + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/compositions/StoreAppBar.kt b/store/src/main/kotlin/com/example/store/ui/compositions/StoreAppBar.kt new file mode 100644 index 00000000..cb17d4a2 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/compositions/StoreAppBar.kt @@ -0,0 +1,81 @@ +package com.example.store.ui.compositions + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.BrightnessAuto +import androidx.compose.material.icons.rounded.DarkMode +import androidx.compose.material.icons.rounded.LightMode +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewLightDark +import com.example.store.theme.StoreAppTheme +import org.imaginativeworld.whynotcompose.base.models.UIThemeMode +import org.imaginativeworld.whynotcompose.base.utils.UIThemeController + +@Composable +fun StoreAppBar( + modifier: Modifier = Modifier, + title: String = "Store Overflow", + goBack: () -> Unit = {}, + toggleUIMode: () -> Unit = {} +) { + val uiThemeMode by UIThemeController.uiThemeMode.collectAsState() + + CenterAlignedTopAppBar( + modifier = modifier, + title = { + Text( + title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.Bold + ) + }, + navigationIcon = { + IconButton(onClick = { + goBack() + }) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = "Go back" + ) + } + }, + actions = { + IconButton(onClick = { + toggleUIMode() + }) { + Icon( + imageVector = when (uiThemeMode) { + UIThemeMode.AUTO -> Icons.Rounded.BrightnessAuto + UIThemeMode.LIGHT -> Icons.Rounded.LightMode + UIThemeMode.DARK -> Icons.Rounded.DarkMode + }, + contentDescription = "" + ) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) +} + +@PreviewLightDark +@Composable +private fun StoreAppBarPreview() { + StoreAppTheme { + StoreAppBar() + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/compositions/TabScreen.kt b/store/src/main/kotlin/com/example/store/ui/compositions/TabScreen.kt new file mode 100644 index 00000000..fa5bad5e --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/compositions/TabScreen.kt @@ -0,0 +1,150 @@ +package com.example.store.ui.compositions + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Category +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.ShoppingCart +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController +import com.example.store.models.categorie.Category +import com.example.store.models.product.Product +import com.example.store.theme.StoreAppTheme +import com.example.store.ui.screen.cart.CartScreen +import com.example.store.ui.screen.categories.CategoriesScreen +import com.example.store.ui.screen.home.StoreHomeScreen +import com.example.store.ui.screen.home.StoreHomeScreenViewModel +import com.example.store.ui.screen.productdetails.dummyProducts + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun TabScreen( + userName: String, + onOrderClick: () -> Unit, + onSignOutClick: () -> Unit, + goBack: () -> Unit, + toggleUIMode: () -> Unit, + onCheckout: () -> Unit, + onProductClick: (Product) -> Unit, + onCategoryClick: (Category) -> Unit +) { + val navController = rememberNavController() + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = navBackStackEntry?.destination?.route + + Scaffold( + bottomBar = { + NavigationBar { + bottomNavigationItems().forEach { navigationItem -> + NavigationBarItem( + selected = currentRoute == navigationItem.route, + label = { Text(navigationItem.label) }, + icon = { + Icon( + navigationItem.icon, + contentDescription = navigationItem.label + ) + }, + onClick = { + if (currentRoute != navigationItem.route) { + navController.navigate(navigationItem.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + } + ) + } + } + }, + contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp) + ) { paddingValues -> + NavHost( + navController = navController, + startDestination = Screens.Home.route, + modifier = Modifier.padding(paddingValues) + ) { + composable(Screens.Home.route) { + val viewModel: StoreHomeScreenViewModel = hiltViewModel() + StoreHomeScreen( + viewModel = viewModel, + userName = userName, + onOrderClick = onOrderClick, + onSignOutClick = onSignOutClick, + toggleUIMode = toggleUIMode, + onProductClick = onProductClick, + onCategoryClick = onCategoryClick + ) + } + composable(Screens.Categories.route) { + CategoriesScreen( + goBack = goBack, + toggleUIMode = toggleUIMode, + onCategoryClick = onCategoryClick + ) + } + composable(Screens.Cart.route) { + CartScreen( + products = dummyProducts, + goBack = goBack, + onCheckout = onCheckout, + toggleUIMode = toggleUIMode + ) + } + } + } +} + +data class NavigationItem( + val label: String, + val icon: ImageVector, + val route: String +) + +fun bottomNavigationItems(): List = listOf( + NavigationItem("Home", Icons.Filled.Home, Screens.Home.route), + NavigationItem("Categories", Icons.Filled.Category, Screens.Categories.route), + NavigationItem("Cart", Icons.Filled.ShoppingCart, Screens.Cart.route) +) + +sealed class Screens(val route: String) { + object Home : Screens("home") + object Categories : Screens("categories") + object Cart : Screens("cart") +} + +@PreviewLightDark +@Composable +private fun TabScreenPreview() { + StoreAppTheme { + TabScreen( + userName = "John Doe", + onOrderClick = {}, + onSignOutClick = {}, + goBack = {}, + toggleUIMode = {}, + onCheckout = {}, + onProductClick = {}, + onCategoryClick = {} + ) + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/screen/StoreMainScreen.kt b/store/src/main/kotlin/com/example/store/ui/screen/StoreMainScreen.kt new file mode 100644 index 00000000..3ed97780 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/StoreMainScreen.kt @@ -0,0 +1,68 @@ +package com.example.store.ui.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.compose.rememberNavController +import com.example.store.theme.StoreAppTheme +import org.imaginativeworld.whynotcompose.base.models.UIThemeMode +import org.imaginativeworld.whynotcompose.base.utils.UIThemeController + +@Composable +fun StoreMainScreen( + updateUiThemeMode: (UIThemeMode) -> Unit, + goBack: () -> Unit +) { + val uiThemeMode by UIThemeController.uiThemeMode.collectAsState() + val isSystemInDarkTheme = isSystemInDarkTheme() + + val isDarkMode by remember(isSystemInDarkTheme) { + derivedStateOf { + when (uiThemeMode) { + UIThemeMode.AUTO -> isSystemInDarkTheme + UIThemeMode.LIGHT -> false + UIThemeMode.DARK -> true + } + } + } + + StoreAppTheme( + darkTheme = isDarkMode + ) { + StoreMainScreenSkeleton( + updateUiThemeMode = updateUiThemeMode, + goBack = goBack + ) + } +} + +@Preview +@Composable +private fun StoreMainScreenSkeletonPreview() { + StoreAppTheme { + StoreMainScreenSkeleton() + } +} + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun StoreMainScreenSkeleton( + updateUiThemeMode: (UIThemeMode) -> Unit = {}, + goBack: () -> Unit = {} +) { + val navController = rememberNavController() + + StoreNavHost( + modifier = Modifier.background(MaterialTheme.colorScheme.background), + navController = navController, + updateUiThemeMode = updateUiThemeMode, + goBack = goBack + ) +} diff --git a/store/src/main/kotlin/com/example/store/ui/screen/StoreNavGraph.kt b/store/src/main/kotlin/com/example/store/ui/screen/StoreNavGraph.kt new file mode 100644 index 00000000..22a06fa8 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/StoreNavGraph.kt @@ -0,0 +1,304 @@ +package com.example.store.ui.screen + +import LoginScreen +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.toRoute +import com.example.store.ui.compositions.TabScreen +import com.example.store.ui.screen.categories.CategoriesScreen +import com.example.store.ui.screen.categorieswiseproduct.CategoriesWiseProductScreen +import com.example.store.ui.screen.checkout.CheckOutScreen +import com.example.store.ui.screen.home.StoreHomeScreen +import com.example.store.ui.screen.home.StoreHomeScreenViewModel +import com.example.store.ui.screen.order.Order +import com.example.store.ui.screen.order.OrdersScreen +import com.example.store.ui.screen.order.dummyOrders +import com.example.store.ui.screen.productdetails.ProductDetailsScreen +import com.example.store.ui.screen.productdetails.ProductDetailsViewModel +import com.example.store.ui.screen.productdetails.dummyProducts +import com.example.store.ui.screen.profile.ProfileScreen +import com.example.store.ui.screen.splash.StoreSplashScreen +import kotlinx.serialization.Serializable +import org.imaginativeworld.whynotcompose.base.models.UIThemeMode +import org.imaginativeworld.whynotcompose.base.models.nextMode +import org.imaginativeworld.whynotcompose.base.utils.UIThemeController + +sealed class SplashScreen { + @Serializable + object Splash +} + +sealed class AuthScreen { + @Serializable + object Login +} + +sealed class MainScreen { + @Serializable + object TabScreen +} + +sealed class StoreScreen { + @Serializable + object StoreHome +} + +@Serializable +sealed class CategorieScreen { + @Serializable + object Categories +} + +@Serializable +sealed class CategoriesWiseProducts { + @Serializable + data class CategoriesWiseProduct(val categoryTitle: String) +} + +@Serializable +sealed class DetailsScreen { + @Serializable + data class ProductDetails(val productId: Int) +} + +@Serializable +sealed class CartScreen { + @Serializable + object Cart +} + +@Serializable +sealed class ProfilesScreen { + @Serializable + object Profile +} + +@Serializable +sealed class OrderScreen { + @Serializable + object Order +} + +@Serializable +sealed class CheckoutScreen { + @Serializable + object Checkout +} + +@Composable +fun StoreNavHost( + navController: NavHostController, + updateUiThemeMode: (UIThemeMode) -> Unit, + goBack: () -> Unit, + modifier: Modifier = Modifier +) { + NavHost( + modifier = modifier, + navController = navController, + startDestination = SplashScreen.Splash + ) { + composable { + StoreSplashScreen( + gotoHomeIndex = { + navController.navigate(AuthScreen.Login) { + popUpTo(SplashScreen.Splash) { inclusive = true } + } + } + ) + } + + composable { + val isDarkMode by UIThemeController.uiThemeMode.collectAsState() + + LoginScreen( + onLogin = { + navController.navigate(MainScreen.TabScreen) { + popUpTo(AuthScreen.Login) { inclusive = true } + } + }, + toggleUIMode = { + updateUiThemeMode(isDarkMode.nextMode()) + } + ) + } + + addStoreScreens( + navController = navController, + updateUiThemeMode = updateUiThemeMode, + goBack = goBack + ) + } +} + +private fun NavGraphBuilder.addStoreScreens( + navController: NavHostController, + updateUiThemeMode: (UIThemeMode) -> Unit, + goBack: () -> Unit +) { + composable { + val isDarkMode by UIThemeController.uiThemeMode.collectAsState() + + TabScreen( + userName = "John Doe", + onOrderClick = { + navController.navigate(OrderScreen.Order) { + popUpTo(MainScreen.TabScreen) { inclusive = true } + } + }, + onSignOutClick = { + navController.navigate(AuthScreen.Login) { + popUpTo(MainScreen.TabScreen) { inclusive = true } + } + }, + goBack = { + navController.popBackStack() + }, + toggleUIMode = { + updateUiThemeMode(isDarkMode.nextMode()) + }, + onProductClick = { product -> + navController.navigate(DetailsScreen.ProductDetails(product.id)) + }, + onCategoryClick = { + navController.navigate(CategoriesWiseProducts.CategoriesWiseProduct(it.name)) + }, + onCheckout = { + navController.navigate(CheckoutScreen.Checkout) + } + ) + } + + composable { + val isDarkMode by UIThemeController.uiThemeMode.collectAsState() + val viewModel: StoreHomeScreenViewModel = hiltViewModel() + StoreHomeScreen( + viewModel = viewModel, + userName = "John Doe", + onOrderClick = { + navController.navigate(OrderScreen.Order) { + popUpTo(MainScreen.TabScreen) { inclusive = true } + } + }, + onSignOutClick = { + navController.navigate(AuthScreen.Login) { + popUpTo(MainScreen.TabScreen) { inclusive = true } + } + }, + toggleUIMode = { + updateUiThemeMode(isDarkMode.nextMode()) + }, + onProductClick = { product -> + navController.navigate(DetailsScreen.ProductDetails(product.id)) + }, + onCategoryClick = { + navController.navigate(CategoriesWiseProducts.CategoriesWiseProduct(it.name)) + } + ) + } + + composable { backStackEntry -> + val productDetails: DetailsScreen.ProductDetails = backStackEntry.toRoute() + val product = dummyProducts.find { it.id == productDetails.productId } + val isDarkMode by UIThemeController.uiThemeMode.collectAsState() + val viewModel: ProductDetailsViewModel = hiltViewModel() + product?.let { + ProductDetailsScreen( + viewModel = viewModel, + product = it, + goBack = { + navController.popBackStack() + }, + toggleUIMode = { + updateUiThemeMode(isDarkMode.nextMode()) + } + ) + } + } + + composable { + val isDarkMode by UIThemeController.uiThemeMode.collectAsState() + CategoriesScreen( + goBack = { + navController.popBackStack() + }, + onCategoryClick = { + navController.navigate(CategoriesWiseProducts.CategoriesWiseProduct(it.name)) + }, + toggleUIMode = { + updateUiThemeMode(isDarkMode.nextMode()) + } + ) + } + + composable { backStackEntry -> + val categoryDetails: CategoriesWiseProducts.CategoriesWiseProduct = backStackEntry.toRoute() + val isDarkMode by UIThemeController.uiThemeMode.collectAsState() + val products = dummyProducts.filter { it.category == categoryDetails.categoryTitle } + CategoriesWiseProductScreen( + products = products, + onProductClick = { product -> + navController.navigate(DetailsScreen.ProductDetails(product.id)) + }, + goBack = { + navController.popBackStack() + }, + toggleUIMode = { + updateUiThemeMode(isDarkMode.nextMode()) + } + ) + } + + composable { + ProfileScreen( + onDismiss = { + navController.popBackStack() + }, + onOrdersClick = { + navController.navigate(OrderScreen.Order) { + popUpTo(MainScreen.TabScreen) { inclusive = true } + } + }, + onSignOutClick = { + navController.navigate(AuthScreen.Login) { + popUpTo(MainScreen.TabScreen) { inclusive = true } + } + } + ) + } + + composable { + val isDarkMode by UIThemeController.uiThemeMode.collectAsState() + OrdersScreen( + orders = dummyOrders, + toggleUIMode = { + updateUiThemeMode(isDarkMode.nextMode()) + }, + goBack = { + navController.navigate(MainScreen.TabScreen) + } + ) + } + + composable { + val isDarkMode by UIThemeController.uiThemeMode.collectAsState() + CheckOutScreen( + goBack = { + navController.navigate(MainScreen.TabScreen) + }, + toggleUIMode = { + updateUiThemeMode(isDarkMode.nextMode()) + }, + goToTab = { + navController.navigate(MainScreen.TabScreen) + } + ) + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/screen/cart/CartScreen.kt b/store/src/main/kotlin/com/example/store/ui/screen/cart/CartScreen.kt new file mode 100644 index 00000000..5a2856ec --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/cart/CartScreen.kt @@ -0,0 +1,166 @@ +package com.example.store.ui.screen.cart + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableDoubleStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.store.theme.StoreAppTheme +import com.example.store.ui.compositions.CartItemCard +import com.example.store.ui.compositions.StoreAppBar +import com.example.store.ui.screen.productdetails.Product +import com.example.store.ui.screen.productdetails.dummyProducts + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun CartScreen( + products: List, + goBack: () -> Unit, + onCheckout: () -> Unit, + toggleUIMode: () -> Unit +) { + CartScreenSkeleton( + products = products, + goBack = goBack, + onCheckout = onCheckout, + toggleUIMode = toggleUIMode + ) +} + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun CartScreenSkeleton( + products: List, + goBack: () -> Unit, + onCheckout: () -> Unit = {}, + toggleUIMode: () -> Unit = {} +) { + Scaffold( + topBar = { + StoreAppBar( + title = "Cart", + goBack = goBack, + toggleUIMode = toggleUIMode + ) + } + ) { paddingValues -> + val context = LocalContext.current + val cartItems = remember { mutableStateListOf(*products.toTypedArray()) } + val totalPrice by remember { mutableDoubleStateOf(cartItems.sumOf { it.price * it.quantity }) } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(horizontal = 16.dp) + ) { + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (cartItems.isEmpty()) { + Text( + text = "Your cart is empty", + fontSize = 18.sp, + color = MaterialTheme.colorScheme.outline + ) + } else { + cartItems.forEach { product -> + CartItemCard( + product = product, + onQuantityChange = { newQuantity -> + if (newQuantity == 0) { + cartItems.remove(product) + } else { + product.quantity = newQuantity + } + } + ) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Total", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + Text( + text = "$${"%.2f".format(totalPrice)}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + TextButton( + onClick = { + Toast.makeText(context, "Order Placed Complete", Toast.LENGTH_SHORT).show() + onCheckout() + }, + modifier = Modifier + .fillMaxWidth(), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), + shape = MaterialTheme.shapes.medium + ) { + Text( + text = "Place Order", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.surface + ) + } + } + } + } +} + +@PreviewLightDark +@Composable +private fun CartScreenSkeletonPreview() { + StoreAppTheme { + CartScreenSkeleton( + products = dummyProducts, + goBack = {}, + onCheckout = {}, + toggleUIMode = {} + ) + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/screen/categories/CategoriesScreen.kt b/store/src/main/kotlin/com/example/store/ui/screen/categories/CategoriesScreen.kt new file mode 100644 index 00000000..d8868205 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/categories/CategoriesScreen.kt @@ -0,0 +1,90 @@ +package com.example.store.ui.screen.categories + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import com.example.store.models.categorie.Category +import com.example.store.models.categorie.categories +import com.example.store.theme.StoreAppTheme +import com.example.store.ui.compositions.CategoryCard +import com.example.store.ui.compositions.StoreAppBar + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun CategoriesScreen( + goBack: () -> Unit, + onCategoryClick: (Category) -> Unit, + toggleUIMode: () -> Unit +) { + CategoriesScreenSkeleton( + goBack = goBack, + onCategoryClick = { + onCategoryClick(it) + }, + toggleUIMode = toggleUIMode + ) +} + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun CategoriesScreenSkeleton( + goBack: () -> Unit = {}, + toggleUIMode: () -> Unit = {}, + onCategoryClick: (Category) -> Unit = {} +) { + Scaffold( + topBar = { + StoreAppBar( + title = "Category", + goBack = goBack, + toggleUIMode = toggleUIMode + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .padding(16.dp) + .fillMaxSize() + ) { + LazyVerticalGrid( + modifier = Modifier, + columns = GridCells.Fixed(2), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(categories.size) { index -> + val category = categories[index] + CategoryCard( + category = category, + modifier = Modifier + .fillMaxWidth(), + onClick = { + onCategoryClick(category) + } + ) + } + } + } + } +} + +@PreviewLightDark +@Composable +private fun CategoriesScreenSkeletonPreview() { + StoreAppTheme { + CategoriesScreenSkeleton( + goBack = {}, + toggleUIMode = {} + ) + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/screen/categories/CategoriesViewModel.kt b/store/src/main/kotlin/com/example/store/ui/screen/categories/CategoriesViewModel.kt new file mode 100644 index 00000000..538f4577 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/categories/CategoriesViewModel.kt @@ -0,0 +1,2 @@ +package com.example.store.ui.screen.categories + diff --git a/store/src/main/kotlin/com/example/store/ui/screen/categorieswiseproduct/CategoriesWiseProductScreen.kt b/store/src/main/kotlin/com/example/store/ui/screen/categorieswiseproduct/CategoriesWiseProductScreen.kt new file mode 100644 index 00000000..a9c5a8a0 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/categorieswiseproduct/CategoriesWiseProductScreen.kt @@ -0,0 +1,76 @@ +package com.example.store.ui.screen.categorieswiseproduct + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.store.theme.StoreAppTheme +import com.example.store.ui.compositions.ProductItem +import com.example.store.ui.compositions.StoreAppBar +import com.example.store.ui.screen.productdetails.Product +import com.example.store.ui.screen.productdetails.dummyProducts + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun CategoriesWiseProductScreen( + products: List, + onProductClick: (Product) -> Unit, + goBack: () -> Unit, + toggleUIMode: () -> Unit +) { + Scaffold( + topBar = { + StoreAppBar( + title = "Category Wise Product", + goBack = goBack, + toggleUIMode = toggleUIMode + ) + } + ) { innerPadding -> + LazyVerticalGrid( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + columns = GridCells.Fixed(2), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { +// items(dummyProducts.size) { index -> +// val product = dummyProducts[index] +// ProductItem( +// product = product, +// onClick = { onProductClick(product) }, +// modifier = Modifier +// .then( +// if (index % 2 == 0) { +// Modifier.padding(start = 16.dp) +// } else { +// Modifier.padding(end = 16.dp) +// } +// +// ) +// ) +// } + } + } +} + +@Preview +@Composable +private fun CategoriesWiseProductScreenPreview() { + StoreAppTheme { + val products = dummyProducts + CategoriesWiseProductScreen( + products = products, + onProductClick = {}, + goBack = {}, + toggleUIMode = {} + ) + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/screen/checkout/CheckOutScreen.kt b/store/src/main/kotlin/com/example/store/ui/screen/checkout/CheckOutScreen.kt new file mode 100644 index 00000000..59f7fca9 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/checkout/CheckOutScreen.kt @@ -0,0 +1,229 @@ +package com.example.store.ui.screen.checkout + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Category +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.store.ui.compositions.OrderProductItem +import com.example.store.ui.compositions.StoreAppBar +import com.example.store.ui.screen.productdetails.Product +import com.example.store.ui.screen.productdetails.dummyProducts + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun CheckOutScreen( + goBack: () -> Unit = {}, + toggleUIMode: () -> Unit = {}, + goToTab: () -> Unit = {} +) { + CheckOutScreenSkeleton( + goBack = goBack, + products = dummyProducts, + toggleUIMode = toggleUIMode, + goToTab = goToTab + ) +} + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun CheckOutScreenSkeleton( + goBack: () -> Unit = {}, + products: List, + toggleUIMode: () -> Unit, + goToTab: () -> Unit = {} +) { + var context = LocalContext.current + var name by remember { mutableStateOf(TextFieldValue("")) } + var phone by remember { mutableStateOf(TextFieldValue("")) } + var address by remember { mutableStateOf(TextFieldValue("")) } + val totalPrice = products.sumOf { it.price } + val productCount = products.size + Scaffold( + topBar = { + StoreAppBar( + goBack = goBack, + title = "Checkout", + toggleUIMode = toggleUIMode + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "\uD83D\uDCCD Shipping Address", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(8.dp)) + + TextField( + value = name, + onValueChange = { name = it }, + placeholder = { Text(text = "Your name...") }, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(12.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + + TextField( + value = phone, + onValueChange = { phone = it }, + placeholder = { Text(text = "Phone number...") }, + modifier = Modifier.fillMaxWidth(), + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(12.dp) + ) + Spacer(modifier = Modifier.height(8.dp)) + + TextField( + value = address, + onValueChange = { address = it }, + placeholder = { Text(text = "Enter your address here...") }, + modifier = Modifier + .fillMaxWidth() + .height( + 130.dp + ), + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(12.dp), + maxLines = 5 + ) + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Category, + contentDescription = "" + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "$productCount Products", + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + products.forEach { product -> + OrderProductItem(product = product) + Spacer(modifier = Modifier.height(12.dp)) + } + } + Spacer(modifier = Modifier.height(16.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Total", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + Text( + text = "$4948.00", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + TextButton( + onClick = { + goToTab() + Toast.makeText(context, "Checkout Successful", Toast.LENGTH_SHORT).show() + }, + modifier = Modifier + .fillMaxWidth(), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), + shape = MaterialTheme.shapes.medium + ) { + Text( + text = "Checkout", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.surface + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} + +@PreviewLightDark +@Composable +private fun PreviewCheckoutScreen() { + StoreAppBar {} + CheckOutScreen() +} diff --git a/store/src/main/kotlin/com/example/store/ui/screen/home/StoreHomeScreen.kt b/store/src/main/kotlin/com/example/store/ui/screen/home/StoreHomeScreen.kt new file mode 100644 index 00000000..ae6283cf --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/home/StoreHomeScreen.kt @@ -0,0 +1,307 @@ +package com.example.store.ui.screen.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.SportsBaseball +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import coil3.compose.AsyncImage +import com.example.store.models.categorie.Category +import com.example.store.models.product.Product +import com.example.store.theme.StoreAppTheme +import com.example.store.ui.compositions.ProductItem +import com.example.store.ui.compositions.StoreAppBar +import com.example.store.ui.screen.profile.ProfileScreen + +@Suppress("ktlint:compose:param-order-check") +@Composable +fun StoreHomeScreen( + viewModel: StoreHomeScreenViewModel, + userName: String, + onOrderClick: () -> Unit, + onSignOutClick: () -> Unit, + onCategoryClick: (Category) -> Unit, + onProductClick: (Product) -> Unit, + toggleUIMode: () -> Unit +) { + val state by viewModel.state.collectAsState() + val pagedProducts = state.items.collectAsLazyPagingItems() + + LaunchedEffect(Unit) { + viewModel.loadCategories() + } + + StoreHomeSkeleton( + userName = userName, + onOrdersClick = onOrderClick, + onSignOutClick = onSignOutClick, + categories = state.categories, + products = pagedProducts, + onCategoryClick = onCategoryClick, + onProductClick = onProductClick, + toggleUIMode = toggleUIMode + ) +} + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun StoreHomeSkeleton( + userName: String, + onOrdersClick: () -> Unit, + onSignOutClick: () -> Unit, + categories: List, + products: LazyPagingItems, + onCategoryClick: (Category) -> Unit, + onProductClick: (Product) -> Unit, + toggleUIMode: () -> Unit +) { + var showProfileSheet by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + StoreAppBar( + toggleUIMode = toggleUIMode + ) + }, + contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp) + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + ) { + LazyVerticalGrid( + columns = GridCells.Fixed(2), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + item(span = { GridItemSpan(maxCurrentLineSpan) }) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(top = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Welcome, $userName", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + modifier = Modifier + ) + + Image( + painter = painterResource(org.imaginativeworld.whynotcompose.common.compose.R.drawable.store), + contentDescription = "", + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .clickable { + showProfileSheet = true + }, + contentScale = ContentScale.Crop + ) + } + } + + item(span = { GridItemSpan(maxCurrentLineSpan) }) { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues( + horizontal = 16.dp + ) + ) { + items(homeScreenImages.size) { + HomeScreenImage( + image = homeScreenImages[it], + modifier = Modifier + .width(300.dp) + .height(150.dp) + .background(MaterialTheme.colorScheme.surface) + ) + } + } + } + + item(span = { GridItemSpan(maxCurrentLineSpan) }) { + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues( + horizontal = 16.dp + ) + ) { + items(categories.size) { + CategoryItem( + categories = categories[it], + modifier = Modifier, + onClick = { + onCategoryClick(categories[it]) + } + ) + } + } + } + items(products.itemCount) { index -> + val product = products[index] + if (product != null) { + ProductItem( + product = product, + onClick = { + onProductClick(product) + }, + modifier = Modifier + .then( + if (index % 2 == 0) { + Modifier.padding(start = 16.dp) + } else { + Modifier.padding(end = 16.dp) + } + ) + ) + } + } + } + } + } + if (showProfileSheet) { + ProfileScreen( + onDismiss = { showProfileSheet = false }, + onOrdersClick = onOrdersClick, + onSignOutClick = onSignOutClick + ) + } +} + +@Composable +fun CategoryItem( + categories: Category, + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Card( + modifier = modifier + .clickable( + onClick = { + onClick() + } + ), + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.outlineVariant) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + vertical = 8.dp, + horizontal = 12.dp + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Outlined.SportsBaseball, + contentDescription = "" + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = categories.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier + ) + } + } +} + +@Composable +fun HomeScreenImage( + image: HomeScreenImage, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + AsyncImage( + model = image.imageUrl, + contentDescription = "Product Image", + modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(12.dp)) + .background(color = MaterialTheme.colorScheme.outline) + ) + } +} + +data class HomeScreenImage( + val imageUrl: String +) + +val homeScreenImages = listOf( + HomeScreenImage(imageUrl = "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"), + HomeScreenImage(imageUrl = "https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_.jpg"), + HomeScreenImage(imageUrl = "https://fakestoreapi.com/img/61IBBVJvSDL._AC_SY879._SX._UX._SY._UY_.jpg") +) + +@PreviewLightDark +@Composable +private fun StoreHomeSkeletonPreview() { + StoreAppTheme { +// StoreHomeSkeleton( +// userName = "Shihab", +// onOrdersClick = {}, +// onSignOutClick = {}, +// categories = categories, +// products = {}, +// onCategoryClick = {}, +// onProductClick = {}, +// toggleUIMode = {} +// ) + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/screen/home/StoreHomeScreenViewModel.kt b/store/src/main/kotlin/com/example/store/ui/screen/home/StoreHomeScreenViewModel.kt new file mode 100644 index 00000000..9b10fdd9 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/home/StoreHomeScreenViewModel.kt @@ -0,0 +1,105 @@ +package com.example.store.ui.screen.home + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.example.store.datasource.ProductPagingSource +import com.example.store.models.categorie.Category +import com.example.store.models.product.Product +import com.example.store.repositories.CategoriesRepository +import com.example.store.repositories.ProductRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.launch +import org.imaginativeworld.whynotcompose.base.models.Event + +@HiltViewModel +class StoreHomeScreenViewModel @Inject constructor( + private val productRepository: ProductRepository, + private val categoriesRepository: CategoriesRepository +) : ViewModel() { + + private val eventShowLoading = MutableStateFlow(false) + private val eventShowMessage = MutableStateFlow?>(null) + private var items = MutableStateFlow>>(emptyFlow()) + private val categories = MutableStateFlow>(emptyList()) + + // ---------------- UI State ------------------ + + private val _state = MutableStateFlow(StoreHomeScreenState()) + val state = _state.asStateFlow() + + // ---------------- Combine States ------------------ + + init { + viewModelScope.launch { + combine( + eventShowLoading, + eventShowMessage, + items, + categories + ) { showLoading, showMessage, items, categories -> + + StoreHomeScreenState( + loading = showLoading, + message = showMessage, + items = items, + categories = categories + ) + }.catch { throwable -> + eventShowMessage.emit(Event("Something went wrong")) + throw throwable + }.collect { + Log.d("Log404", "Collecting UI state: ${it.items}") + _state.value = it + } + } + + loadProducts() + Log.d("Log404", "Collecting UI state: ${loadProducts()}") + } + + // ---------------- Load Products ------------------ + + fun loadProducts() { + items.value = Pager(PagingConfig(pageSize = 10)) { + ProductPagingSource(productRepository) + } + .flow + .cachedIn(viewModelScope) + } + + fun loadCategories() { + viewModelScope.launch { + try { + val response = categoriesRepository.getCategories() + val categoriesList = response?.mapIndexed { index, name -> + Category(id = index + 1, name = name, imageUrl = "") + } ?: emptyList() + + categories.value = categoriesList + } catch (e: Exception) { + eventShowMessage.emit(Event("Failed to load categories")) + } finally { + eventShowLoading.emit(false) + } + } + } +} + +data class StoreHomeScreenState( + val loading: Boolean = false, + val message: Event? = null, + val items: Flow> = emptyFlow(), + val categories: List = emptyList() +) diff --git a/store/src/main/kotlin/com/example/store/ui/screen/login/LogInScreen.kt b/store/src/main/kotlin/com/example/store/ui/screen/login/LogInScreen.kt new file mode 100644 index 00000000..f66ee782 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/login/LogInScreen.kt @@ -0,0 +1,232 @@ +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.store.theme.StoreAppTheme +import com.example.store.ui.compositions.StoreAppBar +import org.imaginativeworld.whynotcompose.common.compose.theme.AppleSystemColor + +@Composable +fun LoginScreen( + onLogin: () -> Unit = {}, + toggleUIMode: () -> Unit = {} +) { + LoginSkeleton( + onLogin = onLogin, + toggleUIMode = toggleUIMode + ) +} + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun LoginSkeleton( + onLogin: () -> Unit, + toggleUIMode: () -> Unit = {} +) { + var username by remember { mutableStateOf("store") } + var password by remember { mutableStateOf("223355") } + var usernameError by rememberSaveable { mutableStateOf("") } + var passwordError by rememberSaveable { mutableStateOf("") } + var passwordVisible by rememberSaveable { mutableStateOf(false) } + + val gradientBrush = Brush.linearGradient( + colors = listOf( + AppleSystemColor.Purple, + AppleSystemColor.Blue + ) + ) + + Scaffold( + topBar = { + StoreAppBar( + toggleUIMode = toggleUIMode + ) + } + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .background(gradientBrush), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Welcome to", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.surface, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Store Overflow", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.surface + ) + } + } + Spacer(modifier = Modifier.height(20.dp)) + Column( + modifier = Modifier.padding(16.dp) + ) { + TextField( + label = { + Text( + text = "Username", + color = MaterialTheme.colorScheme.outline + ) + }, + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(12.dp), + value = username, + onValueChange = { + username = it + usernameError = validateUsername(it) + }, + modifier = Modifier + .fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(12.dp)) + + TextField( + value = password, + onValueChange = { + password = it + passwordError = validatePassword(it) + }, + label = { + Text( + text = "Password", + color = MaterialTheme.colorScheme.outline + ) + }, + colors = TextFieldDefaults.colors( + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(12.dp), + singleLine = true, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), + trailingIcon = { + IconButton( + onClick = { passwordVisible = !passwordVisible } + ) { + Icon( + imageVector = if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff, + contentDescription = "Toggle password visibility" + ) + } + }, + modifier = Modifier + .fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Button( + onClick = onLogin, + modifier = Modifier + .wrapContentSize(), + enabled = true, + colors = ButtonColors( + containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f), + contentColor = MaterialTheme.colorScheme.surface, + disabledContainerColor = MaterialTheme.colorScheme.outlineVariant, + disabledContentColor = MaterialTheme.colorScheme.onPrimaryContainer + ), + shape = RoundedCornerShape(30.dp) + ) { + Text( + text = "Login", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .padding( + vertical = 6.dp, + horizontal = 6.dp + ) + ) + } + } + } + } + } +} + +fun validateUsername(username: String): String = when { + username.isEmpty() -> "" + username.length < 3 -> "Username should be at least 3 characters long" + else -> "" +} + +fun validatePassword(password: String): String = when { + password.isEmpty() -> "" + password.length < 6 -> "Password should be at least 6 characters long" + else -> "" +} + +@PreviewLightDark +@Composable +private fun PreviewLoginScreen() { + StoreAppTheme { + LoginSkeleton( + onLogin = {}, + toggleUIMode = {} + ) + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/screen/order/OrderScreen.kt b/store/src/main/kotlin/com/example/store/ui/screen/order/OrderScreen.kt new file mode 100644 index 00000000..2e8be2fa --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/order/OrderScreen.kt @@ -0,0 +1,124 @@ +package com.example.store.ui.screen.order + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import com.example.store.theme.StoreAppTheme +import com.example.store.ui.compositions.OrderCard +import com.example.store.ui.compositions.StoreAppBar +import com.example.store.ui.screen.productdetails.Product + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun OrdersScreen( + orders: List, + goBack: () -> Unit, + toggleUIMode: () -> Unit +) { + Scaffold( + topBar = { + StoreAppBar( + title = "Orders", + goBack = goBack, + toggleUIMode = toggleUIMode + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentPadding = PaddingValues(16.dp) + ) { + items(orders) { order -> + OrderCard(order = order) + Spacer(modifier = Modifier.height(12.dp)) + } + } + } +} + +data class Order( + val id: Int, + val date: String, + val products: List +) + +var dummyOrders = listOf( + Order( + id = 7, + date = "01 Mar 2020", + listOf( + Product( + category = "T-shirt", + id = 2, + title = "Mens Casual Premium Slim Fit T-Shirts", + imageUrl = "https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_.jpg", + price = 22.3, + rating = 4.5, + reviewCount = 259, + description = "Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing.", + quantity = 8 + ) + ) + ), + + Order( + id = 6, + date = "01 Mar 2020", + listOf( + Product( + category = "Backpack", + id = 1, + title = "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops", + imageUrl = "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg", + price = 109.95, + rating = 4.5, + reviewCount = 120, + description = "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday", + quantity = 10 + ) + ) + ), + + Order( + id = 5, + date = "01 Mar 2020", + listOf( + Product( + category = "Jacket", + id = 3, + title = "Mens Cotton Jacket", + imageUrl = "https://fakestoreapi.com/img/71li-ujtlUL._AC_UX679_.jpg", + price = 55.99, + rating = 4.5, + reviewCount = 500, + description = "Great outerwear jackets for Spring/Autumn/Winter, suitable for many occasions, such as working, hiking, camping, mountain/rock climbing, cycling.", + quantity = 5 + ) + ) + ) +) + +@PreviewLightDark +@Composable +private fun OrdersScreenPreview() { + StoreAppTheme { + OrdersScreen( + orders = dummyOrders, + goBack = {}, + toggleUIMode = {} + ) + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/screen/productdetails/ProductDetailsScreen.kt b/store/src/main/kotlin/com/example/store/ui/screen/productdetails/ProductDetailsScreen.kt new file mode 100644 index 00000000..07698bf4 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/productdetails/ProductDetailsScreen.kt @@ -0,0 +1,341 @@ +package com.example.store.ui.screen.productdetails + +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.example.store.models.product.Product +import com.example.store.theme.StoreAppTheme +import com.example.store.ui.compositions.StoreAppBar + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun ProductDetailsScreen( + viewModel: ProductDetailsViewModel, + product: Product, + goBack: () -> Unit = {}, + toggleUIMode: () -> Unit = {} +) { + LaunchedEffect(Unit) { + viewModel.loadProductDetails(product.id) + Log.d("Log404", "ProductDetailsScreen response:${viewModel.loadProductDetails(product.id)}-------id: ${viewModel.loadProductDetails(1)}") + } + ProductDetailsScreenSkeleton( + product = product, + goBack = goBack, + toggleUIMode = toggleUIMode + ) +} + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun ProductDetailsScreenSkeleton( + product: Product, + goBack: () -> Unit = {}, + toggleUIMode: () -> Unit = {} +) { + Scaffold( + topBar = { + StoreAppBar( + title = "Product Details", + goBack = goBack, + toggleUIMode = toggleUIMode + ) + } + ) { innerPadding -> + var quantity by remember { mutableIntStateOf(0) } + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .padding(innerPadding) + .padding(16.dp) + .verticalScroll(scrollState) + ) { + AsyncImage( + model = product.image, + contentDescription = "Product Image", + modifier = Modifier + .fillMaxWidth() + .height(500.dp) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surface) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = product.title, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(bottom = 8.dp), + fontWeight = FontWeight.Bold, + maxLines = 1 + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Icon( + Icons.Default.Star, + contentDescription = "Rating", + tint = Color.Yellow + ) + Text( + text = ("${product.rating} (${product.reviewCount})"), + modifier = Modifier.padding(start = 4.dp), + color = MaterialTheme.colorScheme.outline + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = product.description, + style = MaterialTheme.typography.bodyLarge + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "$${product.price}", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.Bold + ) + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = { if (quantity > 0) quantity-- }, + modifier = Modifier + ) { + Box( + modifier = Modifier + .padding(4.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color.Blue) + .padding(4.dp) + ) { + Icon( + Icons.Default.Remove, + contentDescription = "Decrease", + tint = MaterialTheme.colorScheme.surface + ) + } + } + + Text( + text = "$quantity", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .padding(horizontal = 8.dp) + ) + + IconButton( + onClick = { quantity++ } + ) { + Box( + modifier = Modifier + .padding(4.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color.Blue) + .padding(4.dp) + ) { + Icon( + Icons.Default.Add, + contentDescription = "Increase", + tint = MaterialTheme.colorScheme.surface + ) + } + } + } + } + } + } +} + +//data class Product( +// val category: String, +// val id: Int, +// val title: String, +// val imageUrl: String, +// val price: Double, +// val rating: Double, +// val reviewCount: Int, +// val description: String, +// var quantity: Int +//) +// +//val dummyProducts = listOf( +// Product( +// category = "Backpack", +// id = 1, +// title = "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops", +// imageUrl = "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg", +// price = 109.95, +// rating = 4.5, +// reviewCount = 120, +// description = "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday", +// quantity = 10 +// ), +// Product( +// category = "T-shirt", +// id = 2, +// title = "Mens Casual Premium Slim Fit T-Shirts", +// imageUrl = "https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_.jpg", +// price = 22.3, +// rating = 4.5, +// reviewCount = 259, +// description = "Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing.", +// quantity = 8 +// ), +// Product( +// category = "Jacket", +// id = 3, +// title = "Mens Cotton Jacket", +// imageUrl = "https://fakestoreapi.com/img/71li-ujtlUL._AC_UX679_.jpg", +// price = 55.99, +// rating = 4.5, +// reviewCount = 500, +// description = "Great outerwear jackets for Spring/Autumn/Winter, suitable for many occasions, such as working, hiking, camping, mountain/rock climbing, cycling.", +// quantity = 5 +// ), +// Product( +// category = "Clothing", +// id = 4, +// title = "Mens Casual Slim Fit", +// imageUrl = "https://fakestoreapi.com/img/71YXzeOuslL._AC_UY879_.jpg", +// price = 15.99, +// rating = 4.5, +// reviewCount = 430, +// description = "The color could be slightly different between on the screen and in practice.", +// quantity = 12 +// ), +// Product( +// category = "Jewelry", +// id = 5, +// title = "John Hardy Women's Legends Naga Gold & Silver Dragon Station Chain Bracelet", +// imageUrl = "https://fakestoreapi.com/img/71pWzhdJNwL._AC_UL640_QL65_ML3_.jpg", +// price = 695.0, +// rating = 4.5, +// reviewCount = 400, +// description = "From our Legends Collection, the Naga was inspired by the mythical water dragon that protects the ocean's pearl.", +// quantity = 7 +// ), +// Product( +// category = "Jewelry", +// id = 6, +// title = "Solid Gold Petite Micropave", +// imageUrl = "https://fakestoreapi.com/img/61sbMiUnoGL._AC_UL640_QL65_ML3_.jpg", +// price = 168.0, +// rating = 4.5, +// reviewCount = 70, +// description = "Satisfaction Guaranteed. Return or exchange any order within 30 days.", +// quantity = 10 +// ), +// Product( +// category = "Jewelry", +// id = 7, +// title = "White Gold Plated Princess", +// imageUrl = "https://fakestoreapi.com/img/71YAIFU48IL._AC_UL640_QL65_ML3_.jpg", +// price = 9.99, +// rating = 4.5, +// reviewCount = 400, +// description = "Classic Created Wedding Engagement Solitaire Diamond Promise Ring for Her.", +// quantity = 15 +// ), +// Product( +// category = "Jewelry", +// id = 8, +// title = "Pierced Owl Rose Gold Plated Stainless Steel Double", +// imageUrl = "https://fakestoreapi.com/img/51UDEzMJVpL._AC_UL640_QL65_ML3_.jpg", +// price = 10.99, +// rating = 4.5, +// reviewCount = 100, +// description = "Rose Gold Plated Double Flared Tunnel Plug Earrings.", +// quantity = 3 +// ), +// Product( +// category = "External Hard Drive", +// id = 9, +// title = "WD 2TB Elements Portable External Hard Drive - USB 3.0", +// imageUrl = "https://fakestoreapi.com/img/61IBBVJvSDL._AC_SY879_.jpg", +// price = 64.0, +// rating = 4.5, +// reviewCount = 203, +// description = "USB 3.0 and USB 2.0 Compatibility Fast data transfers.", +// quantity = 4 +// ), +// Product( +// category = "Storage", +// id = 10, +// title = "SanDisk SSD PLUS 1TB Internal SSD - SATA III 6 Gb/s", +// imageUrl = "https://fakestoreapi.com/img/61U7T1koQqL._AC_SX679_.jpg", +// price = 109.0, +// rating = 4.7, +// reviewCount = 470, +// description = "Easy upgrade for faster boot up, shutdown, application load and response.", +// quantity = 6 +// ) +) + +@Preview(showBackground = true) +@Composable +private fun ProductDetailsScreenSkeletonPreview() { + StoreAppTheme { + val sampleProduct = Product( + category = "Storage", + id = 10, + title = "SanDisk SSD PLUS 1TB Internal SSD - SATA III 6 Gb/s", + imageUrl = "https://fakestoreapi.com/img/61U7T1koQqL._AC_SX679_.jpg", + price = 109.0, + rating = 4.7, + reviewCount = 470, + description = "Easy upgrade for faster boot up, shutdown, application load and response.", + quantity = 6 + ) + + ProductDetailsScreenSkeleton( + product = sampleProduct + ) + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/screen/productdetails/ProductDetailsViewModel.kt b/store/src/main/kotlin/com/example/store/ui/screen/productdetails/ProductDetailsViewModel.kt new file mode 100644 index 00000000..38a5e9dc --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/productdetails/ProductDetailsViewModel.kt @@ -0,0 +1,67 @@ +package com.example.store.ui.screen.productdetails + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.store.repositories.ProductRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch +import org.imaginativeworld.whynotcompose.base.models.Event + +@HiltViewModel +class ProductDetailsViewModel @Inject constructor( + private val productRepository: ProductRepository +) : ViewModel() { + private val eventShowLoading = MutableStateFlow(false) + private val eventShowMessage = MutableStateFlow?>(null) + private val productId = MutableStateFlow(-1) + + private val _state = MutableStateFlow(ProductDetailsScreenState()) + val state = _state.asStateFlow() + + init { + viewModelScope.launch { + combine( + eventShowLoading, + eventShowMessage, + productId + ) { showLoading, showMessage, productId -> + + ProductDetailsScreenState( + loading = showLoading, + message = showMessage, + productId = productId + ) + }.catch { throwable -> + eventShowMessage.emit(Event("Something went wrong")) + throw throwable + }.collect { + _state.value = it + } + } + } + + fun loadProductDetails(id: Int) { + viewModelScope.launch { + try { + val response = productRepository.getProductsId(id) + Log.d("Log404", "loadProductDetails viewMOdel response: $response") + } catch (e: Exception) { + eventShowMessage.emit(Event("Failed to load product details")) + } finally { + eventShowLoading.emit(false) + } + } + } +} + +data class ProductDetailsScreenState( + val loading: Boolean = false, + val message: Event? = null, + val productId: Int = -1 +) diff --git a/store/src/main/kotlin/com/example/store/ui/screen/profile/ProfileScreen.kt b/store/src/main/kotlin/com/example/store/ui/screen/profile/ProfileScreen.kt new file mode 100644 index 00000000..e50c87b4 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/profile/ProfileScreen.kt @@ -0,0 +1,234 @@ +package com.example.store.ui.screen.profile + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.store.theme.StoreAppTheme +import com.example.store.ui.compositions.KeyValue +import kotlinx.coroutines.launch + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun ProfileScreen( + onDismiss: () -> Unit, + onOrdersClick: () -> Unit, + onSignOutClick: () -> Unit +) { + ProfileScreenSkeleton( + onDismiss = onDismiss, + onOrdersClick = onOrdersClick, + onSignOutClick = onSignOutClick + ) +} + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun ProfileScreenSkeleton( + onDismiss: () -> Unit, + onOrdersClick: () -> Unit, + onSignOutClick: () -> Unit +) { + val scope = rememberCoroutineScope() + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ModalBottomSheet( + onDismissRequest = { onDismiss() }, + sheetState = sheetState, + shape = RoundedCornerShape( + topStart = 16.dp, + topEnd = 16.dp + ), + modifier = Modifier + .fillMaxSize() + ) { + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + text = "Profile", + style = MaterialTheme.typography.headlineSmall + ) + }, + actions = { + TextButton( + onClick = { scope.launch { sheetState.hide() }.invokeOnCompletion { onDismiss() } }, + modifier = Modifier + ) { + Text( + text = "Done", + style = MaterialTheme.typography.titleLarge + ) + } + } + ) + }, + contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp) + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(120.dp) + .padding(horizontal = 16.dp) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surfaceContainer), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(org.imaginativeworld.whynotcompose.common.compose.R.drawable.store), + contentDescription = "Profile Image", + modifier = Modifier + .size(100.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + } + + Spacer(modifier = Modifier.height(30.dp)) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) { + Text( + text = "DETAILS", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.outline, + textAlign = TextAlign.Start, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Column( + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(16.dp) + ) { + KeyValue( + title = "Name", + value = "John Doe" + ) + + HorizontalDivider() + + KeyValue( + title = "Username", + value = "johnd" + ) + + HorizontalDivider() + + KeyValue( + title = "Email", + value = "john@gmail.com" + ) + + HorizontalDivider() + + KeyValue( + title = "Phone", + value = "1-570-236-7033" + ) + + HorizontalDivider() + + KeyValue( + title = "Address", + value = "New Road, Kilcoole" + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = onOrdersClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.surfaceContainer), + shape = MaterialTheme.shapes.medium + ) { + Text( + text = "Orders", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleMedium + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = onSignOutClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.surfaceContainer), + shape = MaterialTheme.shapes.medium + ) { + Text( + text = "Sign Out", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.titleMedium + ) + } + } + } + } +} + +@PreviewLightDark +@Composable +private fun ProfileScreenSkeletonPreview() { + StoreAppTheme { + ProfileScreenSkeleton( + onDismiss = {}, + onOrdersClick = {}, + onSignOutClick = {} + ) + } +} diff --git a/store/src/main/kotlin/com/example/store/ui/screen/splash/SplashScreen.kt b/store/src/main/kotlin/com/example/store/ui/screen/splash/SplashScreen.kt new file mode 100644 index 00000000..fc02289c --- /dev/null +++ b/store/src/main/kotlin/com/example/store/ui/screen/splash/SplashScreen.kt @@ -0,0 +1,54 @@ +package com.example.store.ui.screen.splash + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewLightDark +import com.example.store.theme.StoreAppTheme +import kotlinx.coroutines.delay + +@Composable +fun StoreSplashScreen( + gotoHomeIndex: () -> Unit = {} +) { + LaunchedEffect(gotoHomeIndex) { + delay(1000) + + gotoHomeIndex() + } + + StoreSplashScreenSkeleton() +} + +@PreviewLightDark +@Composable +private fun SplashScreenSkeletonPreview() { + StoreAppTheme { + StoreSplashScreenSkeleton() + } +} + +@Suppress("ktlint:compose:modifier-missing-check") +@Composable +fun StoreSplashScreenSkeleton() { + Scaffold { innerPadding -> + Box( + Modifier + .padding(innerPadding) + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Store", + style = MaterialTheme.typography.displayLarge + ) + } + } +} diff --git a/store/src/main/kotlin/com/example/store/utlis/Constants.kt b/store/src/main/kotlin/com/example/store/utlis/Constants.kt new file mode 100644 index 00000000..55a171e2 --- /dev/null +++ b/store/src/main/kotlin/com/example/store/utlis/Constants.kt @@ -0,0 +1,6 @@ +package com.example.store.utlis +class Constants { + companion object { + const val BASE_URL = "https://fakestoreapi.com/" + } +} diff --git a/store/src/main/res/drawable/baseline_visibility_24.xml b/store/src/main/res/drawable/baseline_visibility_24.xml new file mode 100644 index 00000000..b8e84326 --- /dev/null +++ b/store/src/main/res/drawable/baseline_visibility_24.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/store/src/main/res/values/colors.xml b/store/src/main/res/values/colors.xml new file mode 100644 index 00000000..f8c6127d --- /dev/null +++ b/store/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/store/src/main/res/values/strings.xml b/store/src/main/res/values/strings.xml new file mode 100644 index 00000000..6f5da54f --- /dev/null +++ b/store/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Store + \ No newline at end of file diff --git a/store/src/main/res/values/themes.xml b/store/src/main/res/values/themes.xml new file mode 100644 index 00000000..38622fe2 --- /dev/null +++ b/store/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +