Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -38,6 +39,7 @@ android {
buildFeatures {
viewBinding = true
buildConfig = true
compose = true
}
namespace = 'com.cornellappdev.coursegrab'
}
Expand Down Expand Up @@ -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'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the other new versions are pulled into ext. could we do the same here? or maybe share a lifecycle_version with the other lifecycle deps?

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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Course> = 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"
Original file line number Diff line number Diff line change
@@ -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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: OutlinedCard has an onClick overload. using that instead of Modifier.clickable clips the ripple to the rounded shape (right now the ripple goes past the corners) and handles accessibility semantics for us

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()
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
@@ -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) }
}
Loading
Loading