diff --git a/app/build.gradle b/app/build.gradle index 3996160..94f4ec5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget apply plugin: 'com.android.application' apply plugin: 'kotlin-parcelize' apply plugin: 'org.jetbrains.kotlin.plugin.serialization' +apply plugin: 'org.jetbrains.kotlin.plugin.compose' apply plugin: 'com.google.devtools.ksp' apply plugin: 'dagger.hilt.android.plugin' apply plugin: 'com.google.firebase.crashlytics' @@ -38,6 +39,7 @@ android { buildFeatures { viewBinding = true buildConfig = true + compose = true } namespace = 'com.cornellappdev.coursegrab' } @@ -70,7 +72,18 @@ dependencies { implementation 'androidx.core:core-ktx:1.19.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.11.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.11.0' - implementation 'androidx.activity:activity-ktx:1.13.0' + implementation "androidx.activity:activity-ktx:${rootProject.ext.activity_version}" + + // Compose + implementation platform("androidx.compose:compose-bom:${rootProject.ext.compose_bom_version}") + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.ui:ui-graphics' + implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.compose.material3:material3' + implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.11.0' + implementation "androidx.activity:activity-compose:${rootProject.ext.activity_version}" + implementation "androidx.hilt:hilt-navigation-compose:${rootProject.ext.hilt_navigation_compose_version}" + debugImplementation 'androidx.compose.ui:ui-tooling' implementation "com.google.dagger:hilt-android:${rootProject.ext.hilt_version}" ksp "com.google.dagger:hilt-compiler:${rootProject.ext.hilt_version}" implementation 'androidx.constraintlayout:constraintlayout:2.2.2' diff --git a/app/src/main/java/com/cornellappdev/coursegrab/ui/components/CoursePreviewData.kt b/app/src/main/java/com/cornellappdev/coursegrab/ui/components/CoursePreviewData.kt new file mode 100644 index 0000000..e1f863e --- /dev/null +++ b/app/src/main/java/com/cornellappdev/coursegrab/ui/components/CoursePreviewData.kt @@ -0,0 +1,42 @@ +package com.cornellappdev.coursegrab.ui.components + +import com.cornellappdev.coursegrab.models.Course +import com.cornellappdev.coursegrab.models.SearchResult + +internal fun sampleCourse( + catalogNum: Int = 10032, + subjectCode: String = "CS", + courseNum: Int = 1998, + title: String = "Intro to Android Development", + section: String = "LEC 001 / M 7:30PM", + isOpen: Boolean = true, + isTracking: Boolean = false, + numTracking: Int = 12 +): Course = Course( + catalogNum = catalogNum, + courseNum = courseNum, + section = section, + instructors = listOf(SAMPLE_INSTRUCTOR), + isTracking = isTracking, + status = if (isOpen) STATUS_OPEN else STATUS_CLOSED, + subjectCode = subjectCode, + title = title, + numTracking = numTracking +) + +internal fun sampleSearchResult( + subjectCode: String = "CS", + courseNum: Int = 1998, + title: String = "Intro to Android Development", + sections: List = listOf(sampleCourse()) +): SearchResult = SearchResult( + subjectCode = subjectCode, + courseNum = courseNum, + title = title, + sections = sections +) + +internal const val SAMPLE_INSTRUCTOR = "Adrian Sampson (als485)" + +private const val STATUS_OPEN = "OPEN" +private const val STATUS_CLOSED = "CLOSED" diff --git a/app/src/main/java/com/cornellappdev/coursegrab/ui/components/CourseRow.kt b/app/src/main/java/com/cornellappdev/coursegrab/ui/components/CourseRow.kt new file mode 100644 index 0000000..29dac79 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/coursegrab/ui/components/CourseRow.kt @@ -0,0 +1,133 @@ +package com.cornellappdev.coursegrab.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxHeight +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.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.coursegrab.R +import com.cornellappdev.coursegrab.models.Course +import com.cornellappdev.coursegrab.ui.theme.CardBorder +import com.cornellappdev.coursegrab.ui.theme.CourseGrabTheme +import com.cornellappdev.coursegrab.ui.theme.PrimaryText +import java.util.Locale + +@Composable +fun CourseRow( + course: Course, + onClick: () -> Unit, + modifier: Modifier = Modifier, + actions: (@Composable RowScope.() -> Unit)? = null +) { + OutlinedCard( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick), + shape = RoundedCornerShape(6.dp), + border = BorderStroke(1.dp, CardBorder), + colors = CardDefaults.outlinedCardColors( + containerColor = MaterialTheme.colorScheme.surface + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row(verticalAlignment = Alignment.Top) { + Text( + text = stringResource( + R.string.course_title_format, + course.subjectCode, + course.courseNum, + course.title + ), + style = MaterialTheme.typography.titleMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + StatusIndicator(isOpen = course.isOpen, modifier = Modifier.padding(start = 12.dp)) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = course.section.uppercase(Locale.ROOT), + style = MaterialTheme.typography.bodyLarge, + color = PrimaryText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + Text( + text = course.catalogNum.toString(), + style = MaterialTheme.typography.bodyLarge, + color = PrimaryText, + modifier = Modifier.padding(start = 12.dp) + ) + } + if (actions != null) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp) + .height(BUTTON_ROW_HEIGHT), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + content = actions + ) + } + } + } +} + +private val BUTTON_ROW_HEIGHT = 44.dp + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun CourseRowOpenPreview() { + CourseGrabTheme { + CourseRow(course = sampleCourse(), onClick = {}) { + TrackButton( + onClick = {}, + modifier = Modifier + .weight(1f) + .fillMaxHeight() + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun CourseRowClosedPreview() { + CourseGrabTheme { + CourseRow( + course = sampleCourse(isOpen = false, isTracking = true), + onClick = {} + ) { + RemoveButton( + onClick = {}, + modifier = Modifier + .weight(1f) + .fillMaxHeight() + ) + } + } +} diff --git a/app/src/main/java/com/cornellappdev/coursegrab/ui/components/EmptyState.kt b/app/src/main/java/com/cornellappdev/coursegrab/ui/components/EmptyState.kt new file mode 100644 index 0000000..4bee818 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/coursegrab/ui/components/EmptyState.kt @@ -0,0 +1,81 @@ +package com.cornellappdev.coursegrab.ui.components + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.cornellappdev.coursegrab.R +import com.cornellappdev.coursegrab.ui.theme.CourseGrabTheme + +@Composable +fun EmptyState( + @DrawableRes icon: Int, + @StringRes title: Int, + @StringRes subtitle: Int, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 32.dp, vertical = 48.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Image( + painter = painterResource(icon), + contentDescription = null, + modifier = Modifier.size(ICON_SIZE) + ) + Text( + text = stringResource(title), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center + ) + Text( + text = stringResource(subtitle), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } +} + +private val ICON_SIZE = 96.dp + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun EmptyStateNoResultsPreview() { + CourseGrabTheme { + EmptyState( + icon = R.drawable.ic_status_closed, + title = R.string.no_courses_alert, + subtitle = R.string.no_results_alert_subtext_try_another + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun EmptyStateFailedPreview() { + CourseGrabTheme { + EmptyState( + icon = R.drawable.ic_status_warning, + title = R.string.search_failed, + subtitle = R.string.search_failed_subtext + ) + } +} diff --git a/app/src/main/java/com/cornellappdev/coursegrab/ui/components/StatusIndicator.kt b/app/src/main/java/com/cornellappdev/coursegrab/ui/components/StatusIndicator.kt new file mode 100644 index 0000000..6aae9ff --- /dev/null +++ b/app/src/main/java/com/cornellappdev/coursegrab/ui/components/StatusIndicator.kt @@ -0,0 +1,35 @@ +package com.cornellappdev.coursegrab.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.cornellappdev.coursegrab.R +import com.cornellappdev.coursegrab.ui.theme.CourseGrabTheme + +@Composable +fun StatusIndicator(isOpen: Boolean, modifier: Modifier = Modifier) { + Image( + painter = painterResource( + if (isOpen) R.drawable.ic_status_open else R.drawable.ic_status_closed + ), + contentDescription = stringResource( + if (isOpen) R.string.status_open else R.string.status_closed + ), + modifier = modifier + ) +} + +@Preview +@Composable +private fun StatusIndicatorPreview() { + CourseGrabTheme { StatusIndicator(isOpen = true) } +} + +@Preview +@Composable +private fun StatusIndicatorClosedPreview() { + CourseGrabTheme { StatusIndicator(isOpen = false) } +} diff --git a/app/src/main/java/com/cornellappdev/coursegrab/ui/components/TrackingButtons.kt b/app/src/main/java/com/cornellappdev/coursegrab/ui/components/TrackingButtons.kt new file mode 100644 index 0000000..70a4070 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/coursegrab/ui/components/TrackingButtons.kt @@ -0,0 +1,92 @@ +package com.cornellappdev.coursegrab.ui.components + +import androidx.annotation.StringRes +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.cornellappdev.coursegrab.R +import com.cornellappdev.coursegrab.ui.theme.CourseGrabTheme +import com.cornellappdev.coursegrab.ui.theme.RemovalRed +import java.util.Locale + +@Composable +fun TrackButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + OutlinedActionButton( + label = R.string.track_button, + accent = Color.Black, + cornerRadius = 1.dp, + onClick = onClick, + modifier = modifier + ) +} + +@Composable +fun RemoveButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + OutlinedActionButton( + label = R.string.remove_button, + accent = RemovalRed, + cornerRadius = 3.dp, + onClick = onClick, + modifier = modifier + ) +} + +@Composable +private fun OutlinedActionButton( + @StringRes label: Int, + accent: Color, + cornerRadius: Dp, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + OutlinedButton( + onClick = onClick, + modifier = modifier, + shape = RoundedCornerShape(cornerRadius), + border = BorderStroke(1.dp, accent), + colors = ButtonDefaults.outlinedButtonColors( + containerColor = Color.White, + contentColor = accent + ), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 0.dp) + ) { + Text( + text = stringResource(label).uppercase(Locale.ROOT), + style = MaterialTheme.typography.labelMedium + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun TrackButtonPreview() { + CourseGrabTheme { + TrackButton(onClick = {}, modifier = Modifier.height(26.dp)) + } +} + +@Preview(showBackground = true) +@Composable +private fun RemoveButtonPreview() { + CourseGrabTheme { + RemoveButton( + onClick = {}, + modifier = Modifier + .fillMaxWidth() + .height(44.dp) + ) + } +} diff --git a/app/src/main/java/com/cornellappdev/coursegrab/ui/theme/Color.kt b/app/src/main/java/com/cornellappdev/coursegrab/ui/theme/Color.kt new file mode 100644 index 0000000..d0dec42 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/coursegrab/ui/theme/Color.kt @@ -0,0 +1,21 @@ +package com.cornellappdev.coursegrab.ui.theme + +import androidx.compose.ui.graphics.Color + +val Teal = Color(0xFF008577) +val TealDark = Color(0xFF00574B) +val Pink = Color(0xFFD81B60) +val RemovalRed = Color(0xFFCA4238) +val LightText = Color(0xFF9EA7B3) +val MutedText = Color(0xFF616C7A) +val StatusGreen = Color(0xFF47C753) +val DarkEnough = Color(0xFF1C1F23) + +val CardBorder = Color(0xFFD1D5DA) + +val PrimaryText = Color(0xDE000000) + +object StatusColors { + val open = StatusGreen + val closed = RemovalRed +} diff --git a/app/src/main/java/com/cornellappdev/coursegrab/ui/theme/Theme.kt b/app/src/main/java/com/cornellappdev/coursegrab/ui/theme/Theme.kt new file mode 100644 index 0000000..e4d7b8d --- /dev/null +++ b/app/src/main/java/com/cornellappdev/coursegrab/ui/theme/Theme.kt @@ -0,0 +1,30 @@ +package com.cornellappdev.coursegrab.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +private val CourseGrabColorScheme = lightColorScheme( + primary = Teal, + onPrimary = Color.White, + primaryContainer = TealDark, + secondary = Pink, + background = Color.White, + onBackground = Color.Black, + surface = Color.White, + onSurface = Color.Black, + onSurfaceVariant = MutedText, + error = RemovalRed, + onError = Color.White, + outline = LightText +) + +@Composable +fun CourseGrabTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = CourseGrabColorScheme, + typography = CourseGrabTypography, + content = content + ) +} diff --git a/app/src/main/java/com/cornellappdev/coursegrab/ui/theme/Type.kt b/app/src/main/java/com/cornellappdev/coursegrab/ui/theme/Type.kt new file mode 100644 index 0000000..10ae841 --- /dev/null +++ b/app/src/main/java/com/cornellappdev/coursegrab/ui/theme/Type.kt @@ -0,0 +1,35 @@ +package com.cornellappdev.coursegrab.ui.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 + +val CourseGrabTypography = Typography( + headlineSmall = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = 24.sp + ), + titleLarge = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = 20.sp + ), + titleMedium = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = 18.sp + ), + bodyLarge = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 16.sp + ), + labelMedium = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 14.sp + ) +) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c93c206..288940f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,6 +52,8 @@ %d Tracking %d Tracking + Open + Closed %d Result %d Results diff --git a/build.gradle b/build.gradle index 0a13479..830bbcc 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,9 @@ buildscript { ext.hilt_version = '2.60.1' ext.serialization_json_version = '1.9.0' ext.retrofit_version = '3.0.0' + ext.compose_bom_version = '2026.08.00' + ext.activity_version = '1.13.0' + ext.hilt_navigation_compose_version = '1.4.0' repositories { google() mavenCentral() @@ -18,6 +21,7 @@ buildscript { classpath "com.google.devtools.ksp:symbol-processing-gradle-plugin:$ksp_version" classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" + classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files }