-
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
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,38 @@ | ||
| 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 java.time.LocalDateTime | ||
| import java.time.format.DateTimeFormatter | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| @Singleton | ||
| class AvailabilityRepository @Inject constructor( | ||
| private val retrofitInstance: RetrofitInstance | ||
| ) { | ||
| suspend fun getMyAvailability(): UserAvailability { | ||
| 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.sortedBy { it }.map { start -> | ||
| AvailabilitySlot( | ||
| startDate = start.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME), | ||
| endDate = start.plusMinutes(30L) | ||
| .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) | ||
| ) | ||
| } | ||
| } | ||
|
Comment on lines
+22
to
+32
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. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Look for any backend contract docs / sample payloads / tests that pin the expected datetime format.
rg -nP --type=kotlin -C2 'AvailabilitySlot|UpdateAvailabilityRequest|getMyAvailability|updateAvailability'
# And see how the grid defines slot duration so we can share the constant.
rg -nP --type=kotlin -C3 'plusMinutes|slotMinutes|SLOT_|stepMinutes'Repository: cuappdev/resell-android Length of output: 12724 🏁 Script executed: #!/bin/bash
# Search for any SLOT-related constants or Duration definitions
rg -n 'SLOT|MINUTE|DURATION|SLOT_LENGTH|SLOT_HEIGHT' --type=kotlin | head -50Repository: cuappdev/resell-android Length of output: 438 🏁 Script executed: #!/bin/bash
# Search for test files related to AvailabilityRepository or API
fd -e 'Test.kt' -o -e 'Test.java' | xargs rg -l 'Availability' 2>/dev/nullRepository: cuappdev/resell-android Length of output: 2611 🏁 Script executed: #!/bin/bash
# Check if there's any configuration or constants file defining slot length
fd -type f \( -name '*Constants*' -o -name '*Config*' \) --type kotlin | head -20Repository: cuappdev/resell-android Length of output: 238 🏁 Script executed: #!/bin/bash
# Look for ISO_LOCAL_DATE_TIME usage elsewhere to see if this is a pattern
rg 'ISO_LOCAL_DATE_TIME|ISO_OFFSET_DATE_TIME|ZonedDateTime|OffsetDateTime' --type=kotlin -nRepository: cuappdev/resell-android Length of output: 875 🏁 Script executed: #!/bin/bash
# Check Functions.kt to see the ZonedDateTime pattern
sed -n '100,110p' app/src/main/java/com/cornellappdev/resell/android/util/Functions.ktRepository: cuappdev/resell-android Length of output: 448 🏁 Script executed: #!/bin/bash
# Look for Retrofit/Gson/Moshi configuration that might handle datetime serialization
rg -n 'GsonBuilder|JsonDeserializer|JsonSerializer|Moshi|kotlinx.serialization' --type=kotlin -C 3Repository: cuappdev/resell-android Length of output: 23001 🏁 Script executed: #!/bin/bash
# Check if AvailabilitySlot has any serialization annotations
rg -B5 -A5 'data class AvailabilitySlot' app/src/main/java/com/cornellappdev/resell/android/model/api/AvailabilityApiService.ktRepository: cuappdev/resell-android Length of output: 296 🏁 Script executed: #!/antml:bash
# Search for where UserAvailability and AvailabilitySlot are deserialized from backend
rg -n 'UserAvailability|AvailabilityResponse' --type=kotlin -B2 -A2 | head -80Repository: cuappdev/resell-android Length of output: 5497 🏁 Script executed: #!/bin/bash
# Check the full context of AvailabilityViewModel parsing
sed -n '100,115p' app/src/main/java/com/cornellappdev/resell/android/viewmodel/main/AvailabilityViewModel.ktRepository: cuappdev/resell-android Length of output: 496 🏁 Script executed: #!/bin/bash
# Look for Retrofit/Interceptor configuration that might set up custom datetime handling
rg -n 'HttpClient|OkHttpClient|Interceptor|GsonBuilder|Converter' --type=kotlin -B2 -A5Repository: cuappdev/resell-android Length of output: 17562 🏁 Script executed: #!/antml:bash
# Check if there are any test files or examples showing the actual API format
fd -name '*.kt' | xargs rg -l 'AvailabilitySlot.*startDate' | head -5Repository: cuappdev/resell-android Length of output: 298 🏁 Script executed: #!/bin/bash
# Look for any backend API documentation or mock responses
fd -name '*.json' -o -name '*.md' | xargs rg -l 'availability\|Availability' 2>/dev/null | head -10Repository: cuappdev/resell-android Length of output: 298 Fix datetime format mismatch and extract hardcoded slot duration. Two issues must be resolved before production use:
Also replace 🤖 Prompt for AI Agents |
||
|
|
||
| return retrofitInstance.availabilityApi.updateAvailability( | ||
| UpdateAvailabilityRequest(schedule = schedule) | ||
| ).availability | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| 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.graphics.Color | ||
| 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.Style | ||
|
|
||
| // TODO: very hard coded right now, should integrate networking here + implement viewmodel | ||
| @Composable | ||
| fun AvailabilityFilters( | ||
| modifier: Modifier = Modifier | ||
| ) { | ||
| Column( | ||
| modifier = modifier | ||
| .fillMaxWidth() | ||
| .background(color = Color(0xFFF7F3F9)) | ||
| .verticalScroll(rememberScrollState()), | ||
| verticalArrangement = Arrangement.spacedBy(12.dp), | ||
| horizontalAlignment = Alignment.Start | ||
| ) { | ||
|
|
||
| HorizontalDivider() | ||
|
|
||
| ResellSwitchRow( | ||
| title = "Google Calendar Access", | ||
| checked = true, | ||
| enabled = true, | ||
| onCheckedChange = { | ||
| // TODO | ||
| } | ||
| ) | ||
|
|
||
| HorizontalDivider() | ||
|
|
||
| ResellSwitchRow( | ||
| title = "Availability Sharing", | ||
| checked = true, | ||
| enabled = true, | ||
| onCheckedChange = { | ||
| // 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") | ||
| // 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") } | ||
| }} | ||
|
Comment on lines
+78
to
+80
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. Hoist
When you wire this up to the backend, hoist the map (and the two switch values) into 🤖 Prompt for AI Agents |
||
|
|
||
| Column( | ||
| 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 |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| package com.cornellappdev.resell.android.ui.components.availability.helper | ||
|
|
||
| import androidx.compose.foundation.background | ||
| import androidx.compose.foundation.layout.Box | ||
| import androidx.compose.foundation.layout.Column | ||
| import androidx.compose.foundation.layout.Row | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.foundation.shape.RoundedCornerShape | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.ui.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.graphics.Color | ||
| import androidx.compose.ui.text.font.FontWeight | ||
| import androidx.compose.ui.text.style.TextAlign | ||
| import androidx.compose.ui.tooling.preview.Preview | ||
| import androidx.compose.ui.unit.dp | ||
| import com.cornellappdev.resell.android.ui.theme.Style | ||
| import java.time.YearMonth | ||
| import java.time.LocalDate | ||
|
|
||
|
|
||
| @Composable | ||
| fun MonthCalendar( | ||
| currentMonth: YearMonth, | ||
| selectedDates: List<LocalDate>, | ||
| onMonthChange: (YearMonth) -> Unit, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
|
Comment on lines
+24
to
+30
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.
Either add the previous/next month controls (matching the Figma) and invoke Also applies to: 60-101 🤖 Prompt for AI Agents |
||
| val firstDayOfMonth = currentMonth.atDay(1) | ||
| val firstDayOfWeek = firstDayOfMonth.dayOfWeek.value % 7 | ||
| val daysInMonth = currentMonth.lengthOfMonth() | ||
| val dayLabels = listOf("Su", "Mo", "Tu", "We", "Th", "Fr", "Sa") | ||
|
|
||
| val sortedDates = selectedDates.sorted() | ||
| val rangeStart = sortedDates.firstOrNull() | ||
| val rangeEnd = sortedDates.lastOrNull() | ||
|
|
||
| Column( | ||
| modifier | ||
| .background(color = Color(0xfff7f3f9)) | ||
| .padding(16.dp) | ||
| ) { | ||
| Row( | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) { | ||
| dayLabels.forEach { label -> | ||
| Text( | ||
| text = label, | ||
| modifier = Modifier.weight(1f), | ||
| textAlign = TextAlign.Center, | ||
| style = Style.body2, | ||
| fontWeight = FontWeight.SemiBold, | ||
| color = Color(0xFF707070) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| val totalCells = firstDayOfWeek + daysInMonth | ||
| val rows = (totalCells + 6) / 7 | ||
|
|
||
| for (row in 0 until rows) { | ||
| Row( | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .padding(8.dp) | ||
| ) { | ||
| for (col in 0..6) { | ||
| val cellIndex = row * 7 + col | ||
| val day = cellIndex - firstDayOfWeek + 1 | ||
| val date = if (day in 1..daysInMonth) currentMonth.atDay(day) else null | ||
| val isSelected = date != null && date in selectedDates | ||
| val isRangeStart = date == rangeStart | ||
| val isRangeEnd = date == rangeEnd | ||
| val isMiddle = isSelected && !isRangeStart && !isRangeEnd | ||
|
|
||
| Box( | ||
| modifier = Modifier | ||
| .weight(1f) | ||
| .background( | ||
| color = if (isSelected) Color(0xffe4e0e8) else Color.Transparent, | ||
| shape = when { | ||
| isRangeStart && isRangeEnd -> RoundedCornerShape(4.dp) | ||
| isRangeStart -> RoundedCornerShape(topStart = 4.dp, bottomStart = 4.dp) | ||
| isRangeEnd -> RoundedCornerShape(topEnd = 4.dp, bottomEnd = 4.dp) | ||
| isMiddle -> RoundedCornerShape(0.dp) | ||
| else -> RoundedCornerShape(0.dp) | ||
| } | ||
| ) | ||
| .padding(6.dp), | ||
| contentAlignment = Alignment.Center | ||
| ) { | ||
| if (date != null) { | ||
| Text(text = "$day", style = Style.body2) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Preview | ||
| @Composable | ||
| fun MonthCalendarPreview() { | ||
| MonthCalendar( | ||
| currentMonth = YearMonth.of(2026, 4), | ||
| selectedDates = listOf( | ||
| LocalDate.of(2026, 4, 16), | ||
| LocalDate.of(2026, 4, 17), | ||
| LocalDate.of(2026, 4, 18), | ||
| ), | ||
| onMonthChange = {} | ||
| ) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: cuappdev/resell-android
Length of output: 12237
Add
@SerializedNameannotations to match backend JSON keys.The codebase uses
GsonConverterFactory.create()without a custom naming policy, and other API models extensively use@SerializedNameannotations (e.g.,SettingsApiService,LoginApiService,PostsApiService). Fields likeuserId,updatedAt,startDate, andendDateinUserAvailabilityandAvailabilitySlotwill fail to deserialize if the backend uses snake_case naming, which follows the established pattern in this project.Add missing annotations
🤖 Prompt for AI Agents