diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/compose/presentation/components/AppSnackBar.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/compose/presentation/components/AppSnackBar.kt index f32ae535a..73df3c178 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/compose/presentation/components/AppSnackBar.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/compose/presentation/components/AppSnackBar.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.pennapps.labs.pennmobile.compose.presentation.theme.AppColors import com.pennapps.labs.pennmobile.compose.presentation.theme.AppTheme -import com.pennapps.labs.pennmobile.compose.presentation.theme.GilroyFontFamily +import com.pennapps.labs.pennmobile.compose.presentation.theme.cabinFontFamily import com.pennapps.labs.pennmobile.compose.presentation.theme.sfProFontFamily import kotlinx.coroutines.delay @@ -101,7 +101,7 @@ fun AppSnackBar( Text( text = message, color = snackBarContentColor, - fontFamily = GilroyFontFamily, + fontFamily = cabinFontFamily, fontSize = 15.sp, fontWeight = FontWeight.Medium, modifier = Modifier.weight(1f), diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/compose/presentation/theme/Fonts.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/compose/presentation/theme/Fonts.kt index 6159d7b81..d587269d6 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/compose/presentation/theme/Fonts.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/compose/presentation/theme/Fonts.kt @@ -14,6 +14,26 @@ val provider = certificates = R.array.com_google_android_gms_fonts_certs, ) +// Define the GoogleFont references +private val CabinFont = GoogleFont("Cabin") +private val GoogleSansFont = GoogleFont("Google Sans") + +// Cabin Font Family +val cabinFontFamily = + FontFamily( + androidx.compose.ui.text.googlefonts.Font(googleFont = CabinFont, fontProvider = provider, weight = FontWeight.Normal), // Regular + androidx.compose.ui.text.googlefonts.Font(googleFont = CabinFont, fontProvider = provider, weight = FontWeight.Medium), + androidx.compose.ui.text.googlefonts.Font(googleFont = CabinFont, fontProvider = provider, weight = FontWeight.SemiBold), + ) + +// Google Sans Font Family +val googleSansFontFamily = + FontFamily( + androidx.compose.ui.text.googlefonts.Font(googleFont = GoogleSansFont, fontProvider = provider, weight = FontWeight.Normal), // Regular + androidx.compose.ui.text.googlefonts.Font(googleFont = GoogleSansFont, fontProvider = provider, weight = FontWeight.Medium), + androidx.compose.ui.text.googlefonts.Font(googleFont = GoogleSansFont, fontProvider = provider, weight = FontWeight.SemiBold), + ) + val GilroyFontFamily = FontFamily( Font(R.font.gilroy_light, FontWeight.Normal), diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/compose/presentation/theme/Styles.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/compose/presentation/theme/Styles.kt new file mode 100644 index 000000000..0d6114a08 --- /dev/null +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/compose/presentation/theme/Styles.kt @@ -0,0 +1,30 @@ +package com.pennapps.labs.pennmobile.compose.presentation.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + + +/** + * Object to hold custom TextStyles for consistent typography across the app. + */ +object CustomTextStyles { + + /** + * Text style for all headers in the dining hall screen + */ + @Composable + fun DiningHallsHeader(): TextStyle { + return TextStyle( + fontFamily = GilroyFontFamily, + fontWeight = FontWeight.ExtraBold, + fontSize = 20.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 28.sp, + letterSpacing = 0.sp + ) + } + +} \ No newline at end of file diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/di/AppModule.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/di/AppModule.kt index b1f32f9ef..d9debbfa5 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/di/AppModule.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/di/AppModule.kt @@ -1,3 +1,15 @@ +/** + * @file AppModule.kt + * @brief Hilt module for providing application-wide singleton dependencies. + * + * First ever Dagger Hilt module written for Penn Mobile. + * + * This module is responsible for providing foundational objects that are used across + * the entire application lifecycle, such as SharedPreferences and a global CoroutineScope. + * All dependencies provided here are scoped as singletons. + * + * Created by Andrew Chelimo on 2/11/2025 + */ package com.pennapps.labs.pennmobile.di import android.content.Context @@ -14,17 +26,14 @@ import kotlinx.coroutines.SupervisorJob import javax.inject.Qualifier import javax.inject.Singleton -/** - * First ever Dagger Hilt module written for Penn Mobile. - * - * It is responsible for providing foundational objects - * like SharedPreferences and a main-thread CoroutineScope. - * - * Created by Andrew Chelimo on 2/11/2025 - */ @Module @InstallIn(SingletonComponent::class) object AppModule { + /** + * Provides a singleton instance of [SharedPreferences]. + * + * @param context The application context, used to get the default SharedPreferences. + */ @Singleton @Provides fun providesSharedPreferences( @@ -32,7 +41,9 @@ object AppModule { ): SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) /** - * Provides a coroutine scope that is tied to the application lifecycle + * Provides a coroutine scope that is tied to the application lifecycle. + * This scope is configured with a SupervisorJob, ensuring that the failure of one + * child coroutine does not cancel the entire scope. It uses the Main dispatcher. */ @Singleton @Provides @@ -40,6 +51,9 @@ object AppModule { fun providesAppCoroutineScope(): CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) } +/** + * Qualifier to distinguish the application-level CoroutineScope from other scopes. + */ @Qualifier @Retention(AnnotationRetention.RUNTIME) -annotation class AppScope +annotation class AppScope \ No newline at end of file diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/di/NetworkModule.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/di/NetworkModule.kt index 8b8eea3f7..2382f84eb 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/di/NetworkModule.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/di/NetworkModule.kt @@ -1,3 +1,13 @@ +/** + * @file NetworkModule.kt + * @brief Hilt module for providing network-related singleton components. + * + * This module is responsible for setting up and providing all dependencies required for + * network operations throughout the application. It includes the configuration for Gson, + * OkHttpClient, Retrofit, and the specific API service interfaces. All dependencies + * provided here are scoped as singletons to ensure a single, shared instance is used + * across the app. + */ package com.pennapps.labs.pennmobile.di import com.google.gson.Gson @@ -32,6 +42,11 @@ import javax.inject.Singleton object NetworkModule { private const val PENN_MOBILE_BASE_URL = "https://pennmobile.org/api/" + /** + * Provides a customized [Gson] instance for Retrofit. + * This instance is configured with several custom type adapters to handle the specific + * JSON structures from the Penn Mobile API. + */ @Provides @Singleton fun provideGson(): Gson = @@ -84,6 +99,11 @@ object NetworkModule { ) }.create() + /** + * Provides an [HttpLoggingInterceptor] for debugging network requests. + * This interceptor is configured to log the body of network requests and responses, + * which is invaluable for debugging during development. + */ @Provides @Singleton fun provideLoggingInterceptor(): HttpLoggingInterceptor = @@ -91,6 +111,12 @@ object NetworkModule { level = HttpLoggingInterceptor.Level.BODY } + /** + * Provides the application-wide [OkHttpClient]. + * This client is configured with connection timeouts and the logging interceptor. + * + * @param logging The [HttpLoggingInterceptor] to be added for network debugging. + */ @Provides @Singleton fun provideOkHttpClient(logging: HttpLoggingInterceptor): OkHttpClient = @@ -102,6 +128,14 @@ object NetworkModule { .addInterceptor(logging) .build() + /** + * Provides the application-wide [Retrofit] instance. + * This instance is configured with the base URL, the custom OkHttpClient, and + * multiple converter factories to handle different response types. + * + * @param gson The custom [Gson] instance for JSON serialization/deserialization. + * @param client The configured [OkHttpClient] for making requests. + */ @Provides @Singleton fun providesRetrofit( @@ -117,7 +151,13 @@ object NetworkModule { .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .build() + /** + * Provides the [StudentLife] API service interface. + * Retrofit creates an implementation of this interface to handle API calls. + * + * @param retrofit The configured [Retrofit] instance. + */ @Provides @Singleton fun providesStudentLife(retrofit: Retrofit): StudentLife = retrofit.create(StudentLife::class.java) -} +} \ No newline at end of file diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningFragment.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningFragment.kt index 93fc205e2..b26ca3493 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningFragment.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningFragment.kt @@ -1,10 +1,12 @@ package com.pennapps.labs.pennmobile.dining.fragments +import android.annotation.SuppressLint import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.VisibilityThreshold @@ -12,6 +14,8 @@ import androidx.compose.animation.core.spring 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -43,7 +47,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -54,18 +57,18 @@ import com.pennapps.labs.pennmobile.MainActivity import com.pennapps.labs.pennmobile.R import com.pennapps.labs.pennmobile.compose.presentation.components.AppSnackBar import com.pennapps.labs.pennmobile.compose.presentation.theme.AppColors -import com.pennapps.labs.pennmobile.compose.presentation.theme.AppColors.LabelGreen -import com.pennapps.labs.pennmobile.compose.presentation.theme.AppColors.LabelRed import com.pennapps.labs.pennmobile.compose.presentation.theme.AppTheme -import com.pennapps.labs.pennmobile.compose.presentation.theme.GilroyFontFamily +import com.pennapps.labs.pennmobile.compose.presentation.theme.CustomTextStyles +import com.pennapps.labs.pennmobile.compose.presentation.theme.sfProFontFamily import com.pennapps.labs.pennmobile.compose.utils.NetworkUtils import com.pennapps.labs.pennmobile.compose.utils.SnackBarEvent import com.pennapps.labs.pennmobile.dining.classes.DiningHall import com.pennapps.labs.pennmobile.dining.classes.DiningHallSortOrder import com.pennapps.labs.pennmobile.dining.classes.Venue -import com.pennapps.labs.pennmobile.dining.fragments.components.AnimatedPushDropdown import com.pennapps.labs.pennmobile.dining.fragments.components.DiningHallCard import com.pennapps.labs.pennmobile.dining.fragments.components.FavouriteDiningHalls +import com.pennapps.labs.pennmobile.dining.fragments.components.SortDropdown +import com.pennapps.labs.pennmobile.isOnline import dagger.hilt.android.AndroidEntryPoint import rx.schedulers.Schedulers import java.time.LocalDateTime @@ -95,6 +98,7 @@ class DiningFragment : Fragment() { } @OptIn(ExperimentalMaterial3Api::class) + @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun DiningHallListScreen(viewModel: DiningViewModel = hiltViewModel()) { val pullToRefreshState = rememberPullToRefreshState() @@ -102,6 +106,7 @@ class DiningFragment : Fragment() { val allDiningHalls by viewModel.allDiningHalls.collectAsState() val favouriteDiningHalls by viewModel.favouriteDiningHalls.collectAsState(listOf()) + var isOnline by remember { mutableStateOf(null) } var isSortMenuExpanded by remember { mutableStateOf(false) } val currentSortOption by viewModel.sortOrder.collectAsState() @@ -111,8 +116,8 @@ class DiningFragment : Fragment() { val snackBarContainerColor by remember(snackBarEvent) { derivedStateOf { when (snackBarEvent) { - is SnackBarEvent.Success -> LabelGreen - is SnackBarEvent.Error -> LabelRed + is SnackBarEvent.Success -> AppColors.SelectedTabBlue + is SnackBarEvent.Error -> AppColors.LabelRed is SnackBarEvent.None -> Color.Transparent } } @@ -137,7 +142,7 @@ class DiningFragment : Fragment() { ) } }, - ) { paddingValues -> + ) { _ -> val snackBarActionLabel = stringResource(R.string.log_in) @@ -177,14 +182,24 @@ class DiningFragment : Fragment() { // the indicator. pullToRefreshState.animateToHidden() } - Log.d("DiningFragment", "End ofPullToRefreshState: isDataRefreshing is $isDataRefreshing") + Log.d( + "DiningFragment", + "End ofPullToRefreshState: isDataRefreshing is $isDataRefreshing" + ) + } + + + LaunchedEffect(Unit) { + val isOnlineStatus = isOnline(requireContext()) + Log.d("DiningFragment", "isOnline: $isOnlineStatus") + isOnline = isOnlineStatus } Box( modifier = Modifier .fillMaxSize() - .padding(paddingValues), + .padding(), ) { PullToRefreshBox( isRefreshing = isDataRefreshing, @@ -203,65 +218,89 @@ class DiningFragment : Fragment() { ) }, ) { - LazyColumn( + Column( modifier = Modifier .fillMaxWidth() .fillMaxHeight() - .background(MaterialTheme.colorScheme.background) - .padding(horizontal = 6.dp) - .padding(bottom = 42.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), ) { - item { - FavouriteDiningHalls( - diningHalls = favouriteDiningHalls, - toggleFavourite = { viewModel.toggleFavourite(it) }, - openDiningHallMenu = { hall -> navigateToMenuFragment(hall) }, - modifier = - Modifier - .padding(top = 6.dp) - .clip(RoundedCornerShape(4.dp)) - .background(MaterialTheme.colorScheme.background) - .animateContentSize( - spring( - stiffness = Spring.StiffnessLow, - visibilityThreshold = IntSize.VisibilityThreshold, - ), - ), - ) + AnimatedVisibility(isOnline == false) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(AppColors.LabelRed), + horizontalArrangement = Arrangement.Center + ) { + Text( + "Not Connected to the Internet", + fontSize = 14.sp, + fontFamily = sfProFontFamily + ) + } } - item { - Text( - stringResource(R.string.all_dining_halls), - fontFamily = GilroyFontFamily, - fontWeight = FontWeight.ExtraBold, - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 21.sp, - modifier = Modifier.padding(top = 20.dp), - ) - - AnimatedPushDropdown( - modifier = Modifier.padding(top = 8.dp, bottom = 4.dp), - sortMenuExpanded = isSortMenuExpanded, - toggleExpandedMode = { isSortMenuExpanded = !isSortMenuExpanded }, - currentSortOption = currentSortOption, - sortOptions = DiningHallSortOrder.entries, - changeSortOption = { option -> - viewModel.setSortByMethod(option) - isSortMenuExpanded = false - }, - ) - } + LazyColumn( + modifier = + Modifier + .fillMaxWidth() + .weight(1f) + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 6.dp) + .padding(bottom = 42.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + + item { + SortDropdown( + modifier = Modifier.padding(top = 8.dp, bottom = 4.dp), + sortMenuExpanded = isSortMenuExpanded, + toggleExpandedMode = { + isSortMenuExpanded = !isSortMenuExpanded + }, + currentSortOption = currentSortOption, + sortOptions = DiningHallSortOrder.entries, + changeSortOption = { option -> + viewModel.setSortByMethod(option) + isSortMenuExpanded = false + }, + ) + } + + item { + FavouriteDiningHalls( + diningHalls = favouriteDiningHalls, + toggleFavourite = { viewModel.toggleFavourite(it) }, + openDiningHallMenu = { hall -> navigateToMenuFragment(hall) }, + modifier = + Modifier + .padding(top = 6.dp) + .clip(RoundedCornerShape(4.dp)) + .background(MaterialTheme.colorScheme.background) + .animateContentSize( + spring( + stiffness = Spring.StiffnessLow, + visibilityThreshold = IntSize.VisibilityThreshold, + ), + ), + ) + } - items(allDiningHalls) { diningHall -> - DiningHallCard( - diningHall = diningHall, - isFavourite = favouriteDiningHalls.contains(diningHall), - toggleFavourite = { viewModel.toggleFavourite(diningHall) }, - openDiningHallMenu = { hall -> navigateToMenuFragment(hall) }, - ) + item { + Text( + stringResource(R.string.all_dining_halls), + modifier = Modifier.padding(top = 20.dp).padding(start = 6.dp), + style = CustomTextStyles.DiningHallsHeader() + ) + } + + items(allDiningHalls) { diningHall -> + DiningHallCard( + diningHall = diningHall, + isFavourite = favouriteDiningHalls.contains(diningHall), + toggleFavourite = { viewModel.toggleFavourite(diningHall) }, + openDiningHallMenu = { hall -> navigateToMenuFragment(hall) }, + ) + } } } } diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningViewModel.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningViewModel.kt index a90e34aae..c412255a2 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningViewModel.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/DiningViewModel.kt @@ -16,131 +16,190 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import javax.inject.Inject +/** + * Manages the UI state for the dining screen. + * + * This ViewModel is responsible for fetching dining hall information, including a user's + * favorite halls, and providing this data to the UI. It handles user interactions such as + * sorting the dining hall list, marking halls as favorites, and refreshing the data. + * + * @property sp A [SharedPreferences] instance for persisting user preferences, such as the sort order. + * @property diningRepo The repository responsible for fetching dining data from the network and local sources. + */ @HiltViewModel class DiningViewModel - @Inject - constructor( - private val sp: SharedPreferences, - private val diningRepo: DiningRepo, - ) : ViewModel() { - private val _isRefreshing = MutableStateFlow(false) - val isRefreshing: StateFlow = _isRefreshing - - private val _sortOrder = MutableStateFlow(DiningHallSortOrder.Residential) - val sortOrder: StateFlow = _sortOrder - - private val _allDiningHalls = MutableStateFlow>(emptyList()) - val allDiningHalls: StateFlow> = _allDiningHalls - - private val _snackBarEvent = MutableStateFlow(SnackBarEvent.None) - val snackBarEvent: StateFlow = _snackBarEvent - - private val _favouriteDiningHalls = - diningRepo.favouriteDiningHalls.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(), - emptyList(), +@Inject +constructor( + private val sp: SharedPreferences, + private val diningRepo: DiningRepo, +) : ViewModel() { + private val _isRefreshing = MutableStateFlow(false) + + /** Exposes the current refresh status to the UI, allowing for loading indicators to be shown. */ + val isRefreshing: StateFlow = _isRefreshing + + private val _sortOrder = MutableStateFlow(DiningHallSortOrder.Residential) + + /** The current method used for sorting the list of dining halls (both favorites and all dining halls). */ + val sortOrder: StateFlow = _sortOrder + + private val _allDiningHalls = MutableStateFlow>(emptyList()) + + /** A list of all available dining halls, sorted according to the current [sortOrder]. */ + val allDiningHalls: StateFlow> = _allDiningHalls + + private val _snackBarEvent = MutableStateFlow(SnackBarEvent.None) + + /** Represents a one-time event to be shown in a SnackBar, e.g., for success or error messages. */ + val snackBarEvent: StateFlow = _snackBarEvent + + private val _favouriteDiningHalls = + diningRepo.favouriteDiningHalls.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(), + emptyList(), + ) + + /** A derived flow that contains the full [DiningHall] objects for the user's favorite venues. + * Changes dynamically if: user likes or unlikes a dining hall + * user changes the sort order + * */ + val favouriteDiningHalls = + _allDiningHalls.combine(_favouriteDiningHalls) { allDiningHalls, favouriteDiningHalls -> + sortDiningHalls( + allDiningHalls.filter { diningHall -> + favouriteDiningHalls.contains(diningHall.id) + } ) + } - val favouriteDiningHalls = - _favouriteDiningHalls - .map { favouriteIDs -> - allDiningHalls.value.filter { diningHall -> favouriteIDs.contains(diningHall.id) } - } + init { + fetchSortOrder() - init { - fetchSortOrder() + viewModelScope.launch { + diningRepo.fetchAllDiningHalls() + diningRepo.fetchFavouriteDiningHalls() + + diningRepo.allDiningHalls.collect { halls -> + val editableHalls = halls.toMutableList() + DiningHallUtils.getMenus(editableHalls) + _allDiningHalls.value = sortDiningHalls(editableHalls) + } + } + } - viewModelScope.launch { + /** + * Triggers a refresh of all dining data from the repository. + * It updates the [isRefreshing] state to notify the UI about the ongoing operation. + * All operations are performed within the `viewModelScope`. + */ + fun refreshData() = + viewModelScope.launch { + if (!isRefreshing.value) { + _isRefreshing.value = true + fetchSortOrder() + + Log.d("DiningViewModel", "Refreshing data: ${isRefreshing.value}") diningRepo.fetchAllDiningHalls() diningRepo.fetchFavouriteDiningHalls() - diningRepo.allDiningHalls.collect { halls -> - val editableHalls = halls.toMutableList() - DiningHallUtils.getMenus(editableHalls) - sortDiningHalls(editableHalls) - } +// // Simulate a delay for the refresh operation. This delay has to be there otherwise, our refresh call won't work +// // TODO: Remove this delay by making the fetch operations suspend + delay(1000) + _isRefreshing.value = false + Log.d("DiningViewModel", "DoneRefreshing data: ${isRefreshing.value}") } } - fun refreshData() = - viewModelScope.launch { - if (!isRefreshing.value) { - _isRefreshing.value = true - fetchSortOrder() - - Log.d("DiningViewModel", "Refreshing data: ${isRefreshing.value}") - diningRepo.fetchAllDiningHalls() - diningRepo.fetchFavouriteDiningHalls() - - // Simulate a delay for the refresh operation. This delay has to be there otherwise, our refresh call won't work - // TODO: Remove this delay by making the fetch operations suspend - delay(1000) - _isRefreshing.value = false - Log.d("DiningViewModel", "DoneRefreshing data: ${isRefreshing.value}") - } - } + private fun fetchSortOrder() { + _sortOrder.value = + DiningHallSortOrder.fromKey( + sp.getString("dining_sortBy", DiningHallSortOrder.Residential.key), + ) + Log.d("DiningViewModel", "Sort order: ${sortOrder.value}") + } - private fun fetchSortOrder() { - _sortOrder.value = - DiningHallSortOrder.fromKey( - sp.getString("dining_sortBy", DiningHallSortOrder.Residential.key), - ) - Log.d("DiningViewModel", "Sort order: ${sortOrder.value}") + /** + * Updates the sorting preference for the dining hall list. + * This new preference is persisted to [SharedPreferences] and the [allDiningHalls] list is resorted. + * + * @param diningHallSortOrder The new sorting method to be applied. + */ + fun setSortByMethod(diningHallSortOrder: DiningHallSortOrder) { + sp.edit { + putString("dining_sortBy", diningHallSortOrder.key) } - fun setSortByMethod(diningHallSortOrder: DiningHallSortOrder) { - sp.edit { - putString("dining_sortBy", diningHallSortOrder.key) - } + fetchSortOrder() + _allDiningHalls.value = sortDiningHalls(allDiningHalls.value) + } - fetchSortOrder() - sortDiningHalls(allDiningHalls.value) + /** + * Sorts a given list of dining halls based on the current [sortOrder]. + * + * @param halls The list of [DiningHall]s to be sorted. + * @return A new list containing the sorted [DiningHall]s. + */ + fun sortDiningHalls(halls: List) = + halls.sortedWith { diningHall1, diningHall2 -> + DiningHallUtils.compareDiningHallsForSort(sortOrder.value, diningHall1, diningHall2) } - fun sortDiningHalls(halls: List) { - _allDiningHalls.value = - halls.sortedWith { diningHall1, diningHall2 -> - DiningHallUtils.compareDiningHallsForSort(sortOrder.value, diningHall1, diningHall2) - } - } - fun isFavourite(diningHall: DiningHall) = _favouriteDiningHalls.value.contains(diningHall.id) - - fun toggleFavourite(diningHall: DiningHall) = - viewModelScope.launch { - val isFavourite = isFavourite(diningHall) - - val networkResult = - if (isFavourite) { - diningRepo.removeFromFavouriteDiningHalls(diningHall.id) - } else { - diningRepo.addToFavouriteDiningHalls(diningHall.id) - } - - Log.d("DiningViewModel", "Toggling favourite: networkResult is $networkResult") - - if (networkResult.isSuccessful) { - _snackBarEvent.value = - SnackBarEvent.Success( - message = - if (isFavourite) { - "${diningHall.name} removed from favourites" - } else { - "${diningHall.name} added to favourites" - }, - ) - } else if (networkResult is Result.Error) { - _snackBarEvent.value = SnackBarEvent.Error(networkResult.message) + /** + * Checks if a specific dining hall is marked as a favorite. + * + * @param diningHall The dining hall to check. + * @return `true` if the dining hall is a favorite, `false` otherwise. + */ + fun isFavourite(diningHall: DiningHall) = _favouriteDiningHalls.value.contains(diningHall.id) + + /** + * Toggles the favorite status of a dining hall. + * This function communicates with the [diningRepo] to update the backend and local data. + * On completion, it posts a [SnackBarEvent] to inform the user of the result. + * + * @param diningHall The dining hall whose favorite status will be toggled. + */ + fun toggleFavourite(diningHall: DiningHall) = + viewModelScope.launch { + val isFavourite = isFavourite(diningHall) + + val networkResult = + if (isFavourite) { + diningRepo.removeFromFavouriteDiningHalls(diningHall.id) + } else { + diningRepo.addToFavouriteDiningHalls(diningHall.id) } - } - fun resetSnackBarEvent() { - _snackBarEvent.value = SnackBarEvent.None + Log.d("DiningViewModel", "Toggling favourite: networkResult is $networkResult") + + if (networkResult.isSuccessful) { + _snackBarEvent.value = + SnackBarEvent.Success( + message = + if (isFavourite) { + "${diningHall.name} removed from favourites" + } else { + "${diningHall.name} added to favourites" + }, + ) + } else if (networkResult is Result.Error) { + _snackBarEvent.value = SnackBarEvent.Error(networkResult.message) + } } + + /** + * Resets the [snackBarEvent] to its default state. + * This should be called by the UI after a snackbar event has been handled, to prevent it from + * being shown again on configuration changes. + */ + fun resetSnackBarEvent() { + _snackBarEvent.value = SnackBarEvent.None } +} diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/AnimatedDropDown.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/AnimatedDropDown.kt index 752e10343..eb386fbc6 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/AnimatedDropDown.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/AnimatedDropDown.kt @@ -1,3 +1,14 @@ +/** + * @file AnimatedDropDown.kt + * @brief Provides a generic, stateless, animated dropdown component. + * + * This file contains the `AnimatedPushDropdown` composable, a reusable UI component + * designed to display a title and expandable content. The expansion and collapse are + * animated, and the arrow icon rotates to indicate the current state. + * + * As a stateless component, it hoists its `expanded` state and relies on event callbacks, + * making it highly reusable throughout the application. + */ package com.pennapps.labs.pennmobile.dining.fragments.components import androidx.compose.animation.AnimatedVisibility @@ -20,34 +31,37 @@ import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults 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.rotate -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.pennapps.labs.pennmobile.compose.presentation.theme.AppTheme -import com.pennapps.labs.pennmobile.compose.presentation.theme.GilroyFontFamily -import com.pennapps.labs.pennmobile.dining.classes.DiningHallSortOrder + +/** + * A generic, animated dropdown card component. + * + * This composable displays a title row that, when clicked, expands or collapses to reveal + * the content area. The transition is animated, and a dropdown arrow icon rotates to + * reflect the current state. + * + * @param expanded State representing whether the dropdown is currently expanded. + * @param toggleExpandedMode Event lambda invoked when the title row is clicked. + * @param title The composable content to display in the header row of the dropdown. + * @param content The composable content to display in the expandable area. + * @param modifier The [Modifier] to be applied to the Card container. + */ @Composable fun AnimatedPushDropdown( - sortMenuExpanded: Boolean, + expanded: Boolean, toggleExpandedMode: () -> Unit, - currentSortOption: DiningHallSortOrder, - sortOptions: List, - changeSortOption: (DiningHallSortOrder) -> Unit, + title: @Composable () -> Unit, + content: @Composable () -> Unit, modifier: Modifier = Modifier, ) { val rotationAngle by animateFloatAsState( - targetValue = if (sortMenuExpanded) 180f else 0f, + targetValue = if (expanded) 180f else 0f, label = "Dropdown Arrow Rotation", ) @@ -58,8 +72,8 @@ fun AnimatedPushDropdown( elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), colors = CardDefaults.cardColors( - contentColor = MaterialTheme.colorScheme.onSurface, containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, ), shape = RoundedCornerShape(6.dp), ) { @@ -73,13 +87,8 @@ fun AnimatedPushDropdown( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { - Text( - text = "Sort by ${currentSortOption.toDisplayString()}", - fontFamily = GilroyFontFamily, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 16.sp, - ) + title() + Icon( imageVector = Icons.Default.ArrowDropDown, contentDescription = "Toggle Sort Menu", @@ -89,7 +98,7 @@ fun AnimatedPushDropdown( } AnimatedVisibility( - visible = sortMenuExpanded, + visible = expanded, enter = expandVertically(animationSpec = tween(800)) + fadeIn( @@ -106,50 +115,7 @@ fun AnimatedPushDropdown( 800, ), ), - ) { - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp), - ) { - sortOptions.forEach { orderOption -> - Text( - text = orderOption.key, - color = MaterialTheme.colorScheme.onBackground, - fontFamily = GilroyFontFamily, - fontWeight = FontWeight.Normal, - modifier = - Modifier - .clickable { - changeSortOption(orderOption) - // onExpandedChange() // This would also work - }.padding( - vertical = 6.dp, - horizontal = 8.dp, - ).fillMaxWidth(), - ) - } - } - } - } - } -} - -@Preview -@Composable -private fun PreviewAnimatedPushDropdown() = - AppTheme { - Column { - var sortMenuExpanded by remember { mutableStateOf(false) } - var sortOption by remember { mutableStateOf(DiningHallSortOrder.Residential) } - - AnimatedPushDropdown( - sortMenuExpanded, - toggleExpandedMode = { sortMenuExpanded = !sortMenuExpanded }, - currentSortOption = sortOption, - sortOptions = DiningHallSortOrder.entries, - changeSortOption = { sortOption = it }, - ) + ) { content() } } } +} \ No newline at end of file diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/DiningHallCard.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/DiningHallCard.kt index 58f5e8b5a..d7853b53b 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/DiningHallCard.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/DiningHallCard.kt @@ -1,3 +1,12 @@ +/** + * @file DiningHallCard.kt + * @brief Renders a stateless card component for a single dining hall. + * + * This file contains the `DiningHallCard` composable, a UI component that displays + * key information about a dining hall, such as its image, name, open status, hours, + * and whether it is a user favorite. + * + */ package com.pennapps.labs.pennmobile.dining.fragments.components import androidx.compose.foundation.Image @@ -9,11 +18,9 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeightIn +import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -50,6 +57,15 @@ import com.pennapps.labs.pennmobile.dining.classes.Venue import org.joda.time.Instant import org.joda.time.Interval +/** + * A card component that displays information about a single dining hall. + * + * @param diningHall State representing the [DiningHall] data to display. + * @param isFavourite State representing whether this dining hall is marked as a favorite. + * @param toggleFavourite Event lambda invoked when the user taps the favorite icon. + * @param openDiningHallMenu Event lambda invoked when the user taps on the card. + * @param modifier The [Modifier] to be applied to the Card container. + */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun DiningHallCard( @@ -62,7 +78,7 @@ fun DiningHallCard( val cardHeight = 130.dp Card( - modifier = modifier.heightIn(cardHeight), + modifier = modifier.requiredHeight(cardHeight), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface, @@ -81,9 +97,8 @@ fun DiningHallCard( contentDescription = null, modifier = Modifier - .width(150.dp) + .fillMaxHeight() .aspectRatio(4 / 3f) - .requiredHeightIn(min = cardHeight) .clip(RoundedCornerShape(6.dp)), contentScale = ContentScale.Crop, ) @@ -160,7 +175,18 @@ fun DiningHallCard( } } -@Preview +/** + * Provides a preview of the [DiningHallCard] component for development purposes. + * + * This composable is annotated with `@Preview` and sets up the [DiningHallCard] with + * sample data to be rendered in Android Studio's preview pane, for both light and dark themes. + */ +@Preview(name = "Light Mode", showBackground = true) +@Preview( + name = "Dark Mode", + showBackground = true, + uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES +) @Composable private fun PreviewDiningHallCard() { DiningHallCard( @@ -172,17 +198,16 @@ private fun PreviewDiningHallCard() { hours = hashMapOf( "11 AM - 3 PM" to - Interval( - Instant.now().millis, - Instant.now().millis + 1000, - ), + Interval( + Instant.now().millis, + Instant.now().millis + 1000, + ), ), venue = Venue(), image = R.drawable.dining_commons, ), isFavourite = true, toggleFavourite = { }, - openDiningHallMenu = {}, - modifier = Modifier, + openDiningHallMenu = {} ) -} +} \ No newline at end of file diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/FavouriteDiningHalls.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/FavouriteDiningHalls.kt index c7f6b12dd..eb7053d68 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/FavouriteDiningHalls.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/FavouriteDiningHalls.kt @@ -1,15 +1,22 @@ package com.pennapps.labs.pennmobile.dining.fragments.components -import androidx.compose.animation.animateContentSize +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource 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.fillMaxSize 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.ArrowDropDown +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -21,14 +28,15 @@ 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.draw.rotate import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.pennapps.labs.pennmobile.R import com.pennapps.labs.pennmobile.compose.presentation.theme.AppTheme +import com.pennapps.labs.pennmobile.compose.presentation.theme.CustomTextStyles import com.pennapps.labs.pennmobile.compose.presentation.theme.GilroyFontFamily import com.pennapps.labs.pennmobile.dining.classes.DiningHall import com.pennapps.labs.pennmobile.dining.classes.Venue @@ -40,6 +48,16 @@ fun FavouriteDiningHalls( openDiningHallMenu: (DiningHall) -> Unit, modifier: Modifier = Modifier, ) { + // Whether the favorites section is expanded or not. + // Defaults to true + var expanded by remember { mutableStateOf(true) } + + val rotationAngle by animateFloatAsState( + targetValue = if (expanded) 180f else 0f, + label = "Dropdown Arrow Rotation", + ) + + Column( modifier = modifier @@ -47,62 +65,83 @@ fun FavouriteDiningHalls( .background(MaterialTheme.colorScheme.surfaceContainer) .padding(horizontal = 8.dp) .padding(top = 10.dp, bottom = 16.dp) - .animateContentSize(), ) { - Text( - text = stringResource(R.string.favorites), - fontFamily = GilroyFontFamily, - fontWeight = FontWeight.ExtraBold, - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 21.sp, - ) + Row( + Modifier + .padding(vertical = 4.dp) + .fillMaxWidth() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null // Removes the ripple effect + ) { + expanded = !expanded + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.favorites), + modifier = Modifier.weight(1f), + style = CustomTextStyles.DiningHallsHeader() + ) - if (diningHalls.isEmpty()) { - Surface( - modifier = - Modifier - .fillMaxWidth() - .height(140.dp) - .padding(top = 12.dp, bottom = 12.dp), - color = MaterialTheme.colorScheme.background, - shape = RoundedCornerShape(6.dp), - ) { - Box( + Icon( + imageVector = Icons.Default.ArrowDropDown, + contentDescription = "Toggle Sort Menu", + modifier = Modifier.rotate(rotationAngle), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + AnimatedVisibility(expanded) { + if (diningHalls.isEmpty()) { + Surface( modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center, + .fillMaxWidth() + .height(140.dp) + .padding(top = 12.dp, bottom = 12.dp), + color = MaterialTheme.colorScheme.background, + shape = RoundedCornerShape(6.dp), ) { - Text( - text = "Add your favourite dining halls to see them here 🫶🏾", - fontSize = 15.sp, - modifier = Modifier.fillMaxWidth(0.6f), - textAlign = TextAlign.Center, - fontFamily = GilroyFontFamily, - lineHeight = 17.sp, - ) + Box( + modifier = + Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = "Add your favourite dining halls to see them here 🫶🏾", + fontSize = 15.sp, + modifier = Modifier.fillMaxWidth(0.6f), + textAlign = TextAlign.Center, + fontFamily = GilroyFontFamily, + lineHeight = 17.sp, + ) + } } - } - } else { - Column( - Modifier - .fillMaxWidth() - .padding(top = 12.dp, bottom = 4.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), - ) { - diningHalls.forEach { diningHall -> - DiningHallCard( - diningHall = diningHall, - isFavourite = true, - toggleFavourite = { toggleFavourite(diningHall) }, - openDiningHallMenu = openDiningHallMenu, - ) + } else { + Column( + Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 4.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + diningHalls.forEach { diningHall -> + DiningHallCard( + diningHall = diningHall, + isFavourite = true, + toggleFavourite = { toggleFavourite(diningHall) }, + openDiningHallMenu = openDiningHallMenu, + ) + } } } } } } + val TEST_DINING_HALL = DiningHall( 10, @@ -114,7 +153,12 @@ val TEST_DINING_HALL = ) val TEST_LIST_OF_DINING_HALLS = listOf(TEST_DINING_HALL, TEST_DINING_HALL, TEST_DINING_HALL) -@Preview +@Preview(name = "Light Mode", showBackground = true) +@Preview( + name = "Dark Mode", + showBackground = true, + uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES +) @Composable private fun PreviewEmptyFavouriteDiningHalls() = AppTheme { @@ -122,12 +166,17 @@ private fun PreviewEmptyFavouriteDiningHalls() = FavouriteDiningHalls( listOf(), openDiningHallMenu = {}, - toggleFavourite = { hall -> }, + toggleFavourite = { }, ) } } -@Preview +@Preview(name = "Light Mode", showBackground = true) +@Preview( + name = "Dark Mode", + showBackground = true, + uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES +) @Composable private fun PreviewFavouriteDiningHalls() = AppTheme { diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/SortDropdown.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/SortDropdown.kt new file mode 100644 index 000000000..7f68d84d3 --- /dev/null +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/dining/fragments/components/SortDropdown.kt @@ -0,0 +1,143 @@ +/** + * @file SortDropdown.kt + * @brief Renders a stateless dropdown menu component for selecting a sort order. + * + * This file contains the `SortDropdown` composable, a UI component that allows users + * to view the current sort option and expand a list to select a new one. It is designed + * to be stateless, receiving its state and event handlers from a parent ViewModel. + * + * It utilizes the generic `AnimatedPushDropdown` to manage its animation and layout. + * This file also includes preview functions for both light and dark modes. + */ +package com.pennapps.labs.pennmobile.dining.fragments.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +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.Modifier +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 androidx.compose.ui.unit.sp +import com.pennapps.labs.pennmobile.compose.presentation.theme.AppTheme +import com.pennapps.labs.pennmobile.compose.presentation.theme.GilroyFontFamily +import com.pennapps.labs.pennmobile.compose.presentation.theme.googleSansFontFamily +import com.pennapps.labs.pennmobile.dining.classes.DiningHallSortOrder + +/** + * A dropdown component that displays sorting options for the dining hall list. + * + * It displays the currently selected sort order and, when tapped, expands to show a list of + * available sorting options. + * + * @param sortMenuExpanded State representing whether the dropdown is currently expanded. + * @param toggleExpandedMode Event lambda to be invoked when the user taps to expand or collapse the dropdown. + * @param currentSortOption State representing the currently selected [DiningHallSortOrder]. + * @param sortOptions The list of all available [DiningHallSortOrder] options to display. + * @param changeSortOption Event lambda to be invoked when the user selects a new sort option. + * @param modifier The [Modifier] to be applied to this component. + */ +@Composable +fun SortDropdown( + sortMenuExpanded: Boolean, + toggleExpandedMode: () -> Unit, + currentSortOption: DiningHallSortOrder, + sortOptions: List, + changeSortOption: (DiningHallSortOrder) -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedPushDropdown( + expanded = sortMenuExpanded, + toggleExpandedMode = toggleExpandedMode, + title = { + Text( + text = "Sort by ${currentSortOption.toDisplayString()}", + fontFamily = GilroyFontFamily, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 18.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ) + }, + content = { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + ) { + sortOptions.forEach { orderOption -> + Surface( + color = Color(0xFF000000).copy(alpha = 0.05f), + shape = RoundedCornerShape(4.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(bottom = 5.dp) + .clickable { + changeSortOption(orderOption) + } + ) { + Text( + text = orderOption.key, + color = MaterialTheme.colorScheme.onBackground, + fontFamily = googleSansFontFamily, + fontWeight = FontWeight.Normal, + modifier = + Modifier + .padding( + vertical = 6.dp, + horizontal = 8.dp, + ) + .fillMaxWidth(), + ) + } + } + } + }, + modifier = modifier + ) +} + +/** + * Provides a preview of the [SortDropdown] component for development purposes. + * + * This composable is annotated with `@Preview` and sets up the [SortDropdown] with + * sample state to be rendered in Android Studio's preview pane. + */ +@Preview(name = "Light Mode", showBackground = true) +@Preview( + name = "Dark Mode", + showBackground = true, + uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES +) +@Composable +private fun PreviewSortDropdown() = + AppTheme { + Column ( + modifier = Modifier.padding(vertical = 16.dp) + ) { + var sortMenuExpanded by remember { mutableStateOf(true) } + var sortOption by remember { mutableStateOf(DiningHallSortOrder.Residential) } + + SortDropdown( + sortMenuExpanded, + toggleExpandedMode = { sortMenuExpanded = !sortMenuExpanded }, + currentSortOption = sortOption, + sortOptions = DiningHallSortOrder.entries, + changeSortOption = { sortOption = it }, + ) + } + } \ No newline at end of file diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/more/adapters/AboutAdapter.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/more/adapters/AboutAdapter.kt index 00cca53ab..6c8990e2f 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/more/adapters/AboutAdapter.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/more/adapters/AboutAdapter.kt @@ -50,6 +50,7 @@ class AboutAdapter( "Joe MacDougall" -> R.drawable.joe "Baron Ping-Yeh Hsieh" -> R.drawable.baron "David Fu" -> R.drawable.david + "Andrew Chelimo" -> R.drawable.andrew "Kaushik Akula" -> R.drawable.kaushik else -> null } diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/more/fragments/AboutFragment.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/more/fragments/AboutFragment.kt index b49e23e0e..90587dc58 100644 --- a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/more/fragments/AboutFragment.kt +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/more/fragments/AboutFragment.kt @@ -81,18 +81,19 @@ class AboutFragment : Fragment() { binding.alumniRv.layoutManager = GridLayoutManager(context, 3) val members = arrayListOf( - "Rohan Chhaya", - "Julius Snipes", - "Aaron Mei", "Trini Feng", - "Vedha Avali", "Joe MacDougall", "Baron Ping-Yeh Hsieh", "David Fu", + "Andrew Chelimo", "Kaushik Akula", ) val alumni = arrayListOf( + "Rohan Chhaya", + "Julius Snipes", + "Aaron Mei", + "Vedha Avali", "Marta García Ferreiro", "Varun Ramakrishnan", "Sahit Penmatcha", diff --git a/PennMobile/src/main/res/drawable/andrew.jpg b/PennMobile/src/main/res/drawable/andrew.jpg new file mode 100644 index 000000000..62eed2039 Binary files /dev/null and b/PennMobile/src/main/res/drawable/andrew.jpg differ