-
Notifications
You must be signed in to change notification settings - Fork 0
[User Availability] Implement user availability UI and networking stubs. #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f74572e
b619f04
511034e
6bbb26b
5853e51
d06188b
4f0b07c
1c3db00
384a0ff
e4a94fe
27b791a
efea299
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package com.cornellappdev.resell.android.model.api | ||
|
|
||
| import retrofit2.http.Body | ||
| import retrofit2.http.GET | ||
| import retrofit2.http.POST | ||
|
|
||
| interface AvailabilityApiService { | ||
|
|
||
| @GET("availability/") | ||
| suspend fun getMyAvailability(): AvailabilityResponse | ||
|
|
||
| @POST("availability/update/") | ||
| suspend fun updateAvailability( | ||
| @Body request: UpdateAvailabilityRequest | ||
| ): AvailabilityResponse | ||
| } | ||
|
|
||
| data class AvailabilityResponse( | ||
| val availability: UserAvailability | ||
| ) | ||
|
|
||
| data class UserAvailability( | ||
| val id: String, | ||
| val userId: String, | ||
| val schedule: Map<String, List<AvailabilitySlot>>, | ||
| val updatedAt: String | ||
| ) | ||
|
|
||
| data class AvailabilitySlot( | ||
| val startDate: String, | ||
| val endDate: String | ||
| ) | ||
|
|
||
| data class UpdateAvailabilityRequest( | ||
| val schedule: Map<String, List<AvailabilitySlot>> | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package com.cornellappdev.resell.android.model.profile | ||
|
|
||
| import com.cornellappdev.resell.android.model.api.AvailabilitySlot | ||
| import com.cornellappdev.resell.android.model.api.RetrofitInstance | ||
| import com.cornellappdev.resell.android.model.api.UpdateAvailabilityRequest | ||
| import com.cornellappdev.resell.android.model.api.UserAvailability | ||
| import com.cornellappdev.resell.android.ui.components.availability.helper.SLOT_DURATION_MINUTES | ||
| import java.time.LocalDateTime | ||
| import java.time.ZoneId | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| @Singleton | ||
| class AvailabilityRepository @Inject constructor( | ||
| private val retrofitInstance: RetrofitInstance | ||
| ) { | ||
| suspend fun getMyAvailability(): UserAvailability { | ||
|
RyanCheung555 marked this conversation as resolved.
|
||
| return retrofitInstance.availabilityApi.getMyAvailability().availability | ||
| } | ||
|
|
||
| suspend fun updateAvailability(slots: List<LocalDateTime>): UserAvailability { | ||
| // Convert List<LocalDateTime> to Map<"yyyy-MM-dd", List<AvailabilitySlot>> | ||
| val schedule = slots | ||
| .groupBy { it.toLocalDate().toString() } | ||
| .mapValues { (_, daySlots) -> | ||
| daySlots.sorted().map { start -> | ||
| AvailabilitySlot( | ||
| startDate = start.toUtcInstantString(), | ||
| endDate = start.plusMinutes(SLOT_DURATION_MINUTES.toLong()).toUtcInstantString() | ||
| ) | ||
| } | ||
| } | ||
|
RyanCheung555 marked this conversation as resolved.
|
||
|
|
||
| return retrofitInstance.availabilityApi.updateAvailability( | ||
| UpdateAvailabilityRequest(schedule = schedule) | ||
| ).availability | ||
| } | ||
| } | ||
|
|
||
| // The backend stores/returns dates as UTC instants (e.g. "2026-01-23T16:00:00.000Z"), so | ||
| // device-local wall-clock times must be converted to an instant before sending. | ||
| private fun LocalDateTime.toUtcInstantString(): String = | ||
| atZone(ZoneId.systemDefault()).toInstant().toString() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| package com.cornellappdev.resell.android.ui.components.availability.helper | ||
| 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.padding | ||
| import androidx.compose.foundation.rememberScrollState | ||
| import androidx.compose.foundation.verticalScroll | ||
| import androidx.compose.material3.HorizontalDivider | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.mutableStateMapOf | ||
| import androidx.compose.runtime.remember | ||
| import androidx.compose.ui.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.text.font.FontWeight | ||
| import androidx.compose.ui.tooling.preview.Preview | ||
| import androidx.compose.ui.unit.dp | ||
| import com.cornellappdev.resell.android.ui.components.global.ResellCheckboxRow | ||
| import com.cornellappdev.resell.android.ui.components.global.ResellSwitchRow | ||
| import com.cornellappdev.resell.android.ui.theme.AvailabilityPanelBackground | ||
| import com.cornellappdev.resell.android.ui.theme.Style | ||
|
|
||
| // TODO: very hard coded right now, should integrate networking here + implement viewmodel | ||
| // TODO: Figure out if we are still implementing this functionality | ||
| @Composable | ||
| fun AvailabilityFilters( | ||
| modifier: Modifier = Modifier | ||
| ) { | ||
| Column( | ||
| modifier = modifier | ||
| .fillMaxWidth() | ||
| .background(color = AvailabilityPanelBackground) | ||
| .verticalScroll(rememberScrollState()), | ||
|
RyanCheung555 marked this conversation as resolved.
|
||
| verticalArrangement = Arrangement.spacedBy(12.dp), | ||
| horizontalAlignment = Alignment.Start | ||
| ) { | ||
|
|
||
| HorizontalDivider() | ||
|
|
||
| ResellSwitchRow( | ||
| title = "Google Calendar Access", | ||
| checked = true, | ||
| enabled = true, | ||
| onCheckedChange = { | ||
|
RyanCheung555 marked this conversation as resolved.
|
||
| // TODO | ||
| } | ||
| ) | ||
|
|
||
| HorizontalDivider() | ||
|
|
||
| ResellSwitchRow( | ||
| title = "Availability Sharing", | ||
| checked = true, | ||
| enabled = true, | ||
| onCheckedChange = { | ||
|
RyanCheung555 marked this conversation as resolved.
|
||
| // TODO | ||
| } | ||
| ) | ||
|
|
||
| HorizontalDivider() | ||
|
|
||
| Column( | ||
| modifier = Modifier.padding(22.dp), | ||
| verticalArrangement = Arrangement.spacedBy(20.dp) | ||
| ) { | ||
| Text( | ||
| text = "Sub-Calendars", | ||
| style = Style.body1, | ||
| fontWeight = FontWeight.SemiBold | ||
| ) | ||
|
|
||
| // TODO: replace dummy here with actual sub-calendars from the user | ||
| val subCalendars = listOf("Personal", "Leetcode", "Youtube", "Capra") | ||
|
RyanCheung555 marked this conversation as resolved.
|
||
| // TODO: this is just hard coded, make it not hard coded | ||
| val checkedStates = remember { mutableStateMapOf<String, Boolean>().apply { | ||
| subCalendars.forEach { put(it, it != "Personal" && it != "Capra") } | ||
| }} | ||
|
RyanCheung555 marked this conversation as resolved.
|
||
|
|
||
| Column( | ||
|
RyanCheung555 marked this conversation as resolved.
|
||
| verticalArrangement = Arrangement.spacedBy(8.dp) | ||
| ) { | ||
| subCalendars.chunked(2).forEach { pair -> | ||
| Row( | ||
| modifier = Modifier.fillMaxWidth(), | ||
| horizontalArrangement = Arrangement.spacedBy(12.dp), | ||
| verticalAlignment = Alignment.CenterVertically | ||
| ) { | ||
| pair.forEach { name -> | ||
| ResellCheckboxRow( | ||
| title = name, | ||
| checked = checkedStates[name] ?: false, | ||
| enabled = true, | ||
| onCheckedChange = { checkedStates[name] = it }, | ||
| modifier = Modifier.weight(1f) | ||
| ) | ||
| } | ||
| if (pair.size == 1) { | ||
| Spacer(modifier = Modifier.weight(1f)) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Preview | ||
| @Composable | ||
| fun AvailabilityFiltersPreview() { | ||
| AvailabilityFilters() | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -5,19 +5,42 @@ import androidx.compose.ui.geometry.Size | |||||
| import androidx.compose.ui.graphics.drawscope.DrawScope | ||||||
| import androidx.compose.ui.graphics.drawscope.Fill | ||||||
| import androidx.compose.ui.graphics.drawscope.Stroke | ||||||
| import androidx.compose.ui.unit.Dp | ||||||
| import androidx.compose.ui.unit.dp | ||||||
| import com.cornellappdev.resell.android.ui.theme.ResellPurple | ||||||
| import com.cornellappdev.resell.android.ui.theme.Stroke | ||||||
| import com.cornellappdev.resell.android.util.day | ||||||
| import java.time.LocalDate | ||||||
| import java.time.LocalDateTime | ||||||
| import java.time.LocalTime | ||||||
| import java.time.YearMonth | ||||||
| import kotlin.math.floor | ||||||
|
|
||||||
| const val GRID_HEIGHT = 24 | ||||||
| const val SLOT_DURATION_MINUTES = 30 | ||||||
| val gridStartTime: LocalTime = LocalTime.of(9, 0) | ||||||
| val gridStroke = Stroke | ||||||
| val fillColor = ResellPurple | ||||||
|
|
||||||
| /** Minimum horizontal drag distance before a swipe on [MonthCalendar] changes the month. */ | ||||||
| val MonthSwipeThreshold: Dp = 56.dp | ||||||
|
|
||||||
| /** | ||||||
| * Caps [MonthCalendar]'s day-grid at the height of 5 rows (most common case). | ||||||
| * A 6-row month only occurs when the 1st falls on a Fri/Sat in a 30/31-day month, | ||||||
| * so in this case it scrolls internally instead of growing past this, so the | ||||||
| * header stays pinned and the surrounding panel never has to resize. | ||||||
| */ | ||||||
| val MonthCalendarGridMaxHeight: Dp = 248.dp | ||||||
|
|
||||||
| /** Returns a fixed 3-day group containing [date] (1-3, 4-6, ...), rolling into next month if needed. */ | ||||||
| fun dayGroupContaining(date: LocalDate): List<LocalDate> { | ||||||
| val month = YearMonth.from(date) | ||||||
| val groupIndex = (date.dayOfMonth - 1) / 3 | ||||||
| val groupStart = month.atDay(groupIndex * 3 + 1) | ||||||
| return (0..2).map { groupStart.plusDays(it.toLong()) } | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| fun getGridCell(offset: Offset, canvasSize: Size, width: Int, height: Int): Pair<Int, Int> { | ||||||
| val gridCol = floor(offset.x / (canvasSize.width / width)).toInt().coerceIn(0, width - 1) | ||||||
|
|
@@ -43,7 +66,7 @@ fun rowColToLocalDateTime(row: Int, col: Int, dates: List<LocalDate>): LocalDate | |||||
| .withMinute(gridStartTime.minute) | ||||||
| .withSecond(gridStartTime.second) | ||||||
| .withNano(gridStartTime.nano) | ||||||
| .plusMinutes(30L * row) | ||||||
| .plusMinutes(SLOT_DURATION_MINUTES.toLong() * row) | ||||||
| } | ||||||
|
|
||||||
| fun getTimeForRow(row: Int): LocalTime { | ||||||
|
|
@@ -59,7 +82,7 @@ fun List<LocalDateTime>.mapToGrid(dates: List<LocalDate>): List<BooleanArray> { | |||||
| forEach { date -> | ||||||
| val column = dates.indexOfFirst { it.day == date.day } | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '45,105p' app/src/main/java/com/cornellappdev/resell/android/ui/components/availability/helper/AvailabilityUtil.kt
rg -n "mapToGrid|setSelectedAvailabilities|onAvailabilitySelected|onProposalSelected" app/src/main/java/com/cornellappdev/resell/android/ui/components/availability app/src/main/java/com/cornellappdev/resell/android/ui/screens/main app/src/main/java/com/cornellappdev/resell/android/viewmodel/mainRepository: cuappdev/resell-android Length of output: 6881 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- SelectableAvailabilityGrid ---'
sed -n '205,290p' app/src/main/java/com/cornellappdev/resell/android/ui/components/availability/helper/SelectableAvailabilityGrid.kt
printf '%s\n' '--- SelectableAvailabilityPager ---'
sed -n '1,110p' app/src/main/java/com/cornellappdev/resell/android/ui/components/availability/SelectableAvailabilityPager.kt
printf '%s\n' '--- AvailabilityViewModel ---'
sed -n '1,110p' app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/AvailabilityViewModel.kt
printf '%s\n' '--- AvailabilityScreen date/group wiring ---'
sed -n '50,180p' app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/AvailabilityScreen.kt
printf '%s\n' '--- date/group declarations and call sites ---'
rg -n -C 4 "dates|LocalDate\\.now|plusDays|group|AvailabilityPager|SelectableAvailabilityPager" app/src/main/java/com/cornellappdev/resell/android/ui/components/availability app/src/main/java/com/cornellappdev/resell/android/ui/screens/main app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/AvailabilityViewModel.ktRepository: cuappdev/resell-android Length of output: 50379 🏁 Script executed: #!/bin/bash
set -e
sed -n '205,290p' app/src/main/java/com/cornellappdev/resell/android/ui/components/availability/helper/SelectableAvailabilityGrid.kt
sed -n '1,110p' app/src/main/java/com/cornellappdev/resell/android/ui/components/availability/SelectableAvailabilityPager.kt
sed -n '1,110p' app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/AvailabilityViewModel.kt
sed -n '50,180p' app/src/main/java/com/cornellappdev/resell/android/ui/screens/main/AvailabilityScreen.kt
rg -n -C 4 "dates|LocalDate\.now|plusDays|group|AvailabilityPager|SelectableAvailabilityPager" app/src/main/java/com/cornellappdev/resell/android/ui/components/availability app/src/main/java/com/cornellappdev/resell/android/ui/screens/main app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/AvailabilityViewModel.ktRepository: cuappdev/resell-android Length of output: 50379 Match slots by full date. For Proposed fix- val column = dates.indexOfFirst { it.day == date.day }
+ val column = dates.indexOf(date.toLocalDate())📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| if (column == -1) return@forEach | ||||||
| val row = (date.hour * 60 + date.minute - gridStartTime.hour * 60) / 30 | ||||||
| val row = (date.hour * 60 + date.minute - gridStartTime.hour * 60) / SLOT_DURATION_MINUTES | ||||||
| if (row !in 0 until GRID_HEIGHT) return@forEach | ||||||
| grid[row][column] = true | ||||||
| } | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.