Skip to content

Commit

Permalink
Merge pull request #113 from waseefakhtar/feature/calendar-header
Browse files Browse the repository at this point in the history
Feature: Calendar header in Home
  • Loading branch information
waseefakhtar authored Nov 15, 2023
2 parents 2afedb4 + 5d80fb5 commit 1657795
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ object AnalyticsEvents {

const val HOME_TAB_CLICKED = "home_tab_clicked"
const val HISTORY_TAB_CLICKED = "history_tab_clicked"

const val HOME_CALENDAR_PREVIOUS_WEEK_CLICKED = "home_calendar_previous_week_clicked"
const val HOME_CALENDAR_NEXT_WEEK_CLICKED = "home_calendar_next_week_clicked"
const val HOME_NEW_DATE_SELECTED = "home_new_date_clicked"
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ fun Date.toFormattedDateString(): String {
val sdf = SimpleDateFormat("EEEE, LLLL dd", Locale.getDefault())
return sdf.format(this)
}

fun Date.toFormattedMonthDateString(): String {
val sdf = SimpleDateFormat("MMMM dd", Locale.getDefault())
return sdf.format(this)
}

fun Date.toFormattedDateShortString(): String {
val sdf = SimpleDateFormat("dd", Locale.getDefault())
return sdf.format(this)
}

fun Long.toFormattedDateString(): String {
val sdf = SimpleDateFormat("LLLL dd, yyyy", Locale.getDefault())
return sdf.format(this)
Expand Down
283 changes: 201 additions & 82 deletions app/src/main/java/com/waseefakhtar/doseapp/feature/home/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,44 @@ package com.waseefakhtar.doseapp.feature.home
import android.Manifest
import android.os.Build
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults.cardColors
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
Expand All @@ -46,10 +51,16 @@ import com.waseefakhtar.doseapp.R
import com.waseefakhtar.doseapp.analytics.AnalyticsEvents
import com.waseefakhtar.doseapp.analytics.AnalyticsHelper
import com.waseefakhtar.doseapp.domain.model.Medication
import com.waseefakhtar.doseapp.extension.toFormattedDateShortString
import com.waseefakhtar.doseapp.extension.toFormattedDateString
import com.waseefakhtar.doseapp.extension.toFormattedMonthDateString
import com.waseefakhtar.doseapp.feature.addmedication.navigation.AddMedicationDestination
import com.waseefakhtar.doseapp.feature.home.data.CalendarDataSource
import com.waseefakhtar.doseapp.feature.home.model.CalendarModel
import com.waseefakhtar.doseapp.feature.home.viewmodel.HomeState
import com.waseefakhtar.doseapp.feature.home.viewmodel.HomeViewModel
import java.util.Calendar
import java.util.Date

@Composable
fun HomeRoute(
Expand Down Expand Up @@ -156,7 +167,7 @@ fun EmptyCard(navController: NavController, analyticsHelper: AnalyticsHelper) {
Card(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
.height(156.dp),
shape = RoundedCornerShape(36.dp),
colors = cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
Expand All @@ -171,12 +182,13 @@ fun EmptyCard(navController: NavController, analyticsHelper: AnalyticsHelper) {
Column(
modifier = Modifier
.padding(24.dp, 24.dp, 0.dp, 16.dp)
.fillMaxWidth(.50F),
verticalArrangement = Arrangement.spacedBy(8.dp)
.fillMaxWidth(.50F)
.align(Alignment.CenterVertically),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {

Text(
text = stringResource(R.string.welcome),
text = stringResource(R.string.medication_break),
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge,
)
Expand All @@ -200,97 +212,204 @@ fun EmptyCard(navController: NavController, analyticsHelper: AnalyticsHelper) {
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DailyMedications(navController: NavController, analyticsHelper: AnalyticsHelper, state: HomeState, viewModel: HomeViewModel, navigateToMedicationDetail: (Medication) -> Unit) {

val medicationList = state.medications.sortedBy { it.medicationTime }
val combinedList: List<MedicationListItem> = mutableListOf<MedicationListItem>().apply {
val calendar = Calendar.getInstance()
val medicationsToday = medicationList.filter {
val medicationDate = it.medicationTime
calendar.time = medicationDate
val medicationDay = calendar.get(Calendar.DAY_OF_YEAR)
var filteredMedications: List<Medication> by remember { mutableStateOf(emptyList()) }

val todayCalendar = Calendar.getInstance()
val todayDay = todayCalendar.get(Calendar.DAY_OF_YEAR)
DatesHeader(analyticsHelper) { selectedDate ->
val newMedicationList = state.medications
.filter { medication ->
medication.medicationTime.toFormattedDateString() == selectedDate.date.toFormattedDateString()
}
.sortedBy { it.medicationTime }

medicationDay == todayDay
}
filteredMedications = newMedicationList
analyticsHelper.logEvent(AnalyticsEvents.HOME_NEW_DATE_SELECTED)
}

if (medicationsToday.isNotEmpty()) {
add(MedicationListItem.HeaderItem(stringResource(R.string.today)))
addAll(medicationsToday.map { MedicationListItem.MedicationItem(it) })
if (filteredMedications.isEmpty()) {
EmptyCard(navController, analyticsHelper)
} else {
LazyColumn(
modifier = Modifier,
) {
items(
items = filteredMedications,
itemContent = {
MedicationCard(
medication = it,
navigateToMedicationDetail = { medication ->
navigateToMedicationDetail(medication)
}
)
}
)
}
}
}

// Find medications for this week and add "This Week" header
val startOfWeekThisWeek = Calendar.getInstance()
startOfWeekThisWeek.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
val endOfWeekThisWeek = startOfWeekThisWeek.clone() as Calendar
endOfWeekThisWeek.add(Calendar.DAY_OF_WEEK, 6)
val medicationsThisWeek = medicationList.filter {
val medicationDate = it.medicationTime // Change this to the appropriate attribute
medicationDate in startOfWeekThisWeek.time..endOfWeekThisWeek.time && !medicationsToday.contains(it)
}
if (medicationsThisWeek.isNotEmpty()) {
add(MedicationListItem.HeaderItem(stringResource(R.string.this_week)))
addAll(medicationsThisWeek.map { MedicationListItem.MedicationItem(it) })
}
@Composable
fun DatesHeader(
analyticsHelper: AnalyticsHelper,
onDateSelected: (CalendarModel.DateModel) -> Unit // Callback to pass the selected date
) {
val dataSource = CalendarDataSource()
var calendarModel by remember { mutableStateOf(dataSource.getData(lastSelectedDate = dataSource.today)) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
DateHeader(
data = calendarModel,
onPrevClickListener = { startDate ->
// refresh the CalendarModel with new data
// by get data with new Start Date (which is the startDate-1 from the visibleDates)
val calendar = Calendar.getInstance()
calendar.time = startDate

// Find medications for next week and add "Next Week" header
val startOfWeekNextWeek = Calendar.getInstance()
startOfWeekNextWeek.time = endOfWeekThisWeek.time // Use the end of current week as start of next week
startOfWeekNextWeek.add(Calendar.DAY_OF_MONTH, 1)
val endOfWeekNextWeek = startOfWeekNextWeek.clone() as Calendar
endOfWeekNextWeek.add(Calendar.DAY_OF_MONTH, 6)
val medicationsNextWeek = medicationList.filter {
val medicationDate = it.medicationTime // Change this to the appropriate attribute
medicationDate in startOfWeekNextWeek.time..endOfWeekNextWeek.time
}
if (medicationsNextWeek.isNotEmpty()) {
add(MedicationListItem.HeaderItem(stringResource(R.string.next_week)))
addAll(medicationsNextWeek.map { MedicationListItem.MedicationItem(it) })
}
calendar.add(Calendar.DAY_OF_YEAR, -2) // Subtract one day from startDate
val finalStartDate = calendar.time

val hasMedicationItem = any { it is MedicationListItem.MedicationItem }
add(0, MedicationListItem.OverviewItem(medicationsToday, !hasMedicationItem))
}
calendarModel = dataSource.getData(startDate = finalStartDate, lastSelectedDate = calendarModel.selectedDate.date)
analyticsHelper.logEvent(AnalyticsEvents.HOME_CALENDAR_PREVIOUS_WEEK_CLICKED)
},
onNextClickListener = { endDate ->
// refresh the CalendarModel with new data
// by get data with new Start Date (which is the endDate+2 from the visibleDates)
val calendar = Calendar.getInstance()
calendar.time = endDate

LazyColumn(
modifier = Modifier,
contentPadding = PaddingValues(vertical = 8.dp)
) {
items(
items = combinedList,
itemContent = {
when (it) {
is MedicationListItem.OverviewItem -> {
when (it.isMedicationListEmpty) {
true -> EmptyCard(navController, analyticsHelper)
false -> DailyOverviewCard(navController, analyticsHelper, it.medicationsToday)
}
}
is MedicationListItem.HeaderItem -> {
Text(
modifier = Modifier
.padding(4.dp, 12.dp, 8.dp, 0.dp)
.fillMaxWidth(),
text = it.headerText.uppercase(),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleMedium,
)
}
is MedicationListItem.MedicationItem -> {
MedicationCard(
medication = it.medication,
navigateToMedicationDetail = { medication ->
navigateToMedicationDetail(medication)
}
calendar.add(Calendar.DAY_OF_YEAR, 2)
val finalStartDate = calendar.time

calendarModel = dataSource.getData(startDate = finalStartDate, lastSelectedDate = calendarModel.selectedDate.date)
analyticsHelper.logEvent(AnalyticsEvents.HOME_CALENDAR_NEXT_WEEK_CLICKED)
}
)
DateList(
data = calendarModel,
onDateClickListener = { date ->
calendarModel = calendarModel.copy(
selectedDate = date,
visibleDates = calendarModel.visibleDates.map {
it.copy(
isSelected = it.date.toFormattedDateString() == date.date.toFormattedDateString()
)
}
)
onDateSelected(date)
}
)
}
}

@Composable
fun DateList(
data: CalendarModel,
onDateClickListener: (CalendarModel.DateModel) -> Unit
) {
LazyRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
items(items = data.visibleDates) { date ->
DateItem(date, onDateClickListener)
}
}
}

@Composable
fun DateItem(
date: CalendarModel.DateModel,
onClickListener: (CalendarModel.DateModel) -> Unit,
) {
Column {
Text(
text = date.day, // day "Mon", "Tue"
modifier = Modifier.align(Alignment.CenterHorizontally),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Normal,
color = MaterialTheme.colorScheme.outline
)
Card(
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 4.dp)
.clickable { onClickListener(date) },
colors = cardColors(
// background colors of the selected date
// and the non-selected date are different
containerColor = if (date.isSelected) {
MaterialTheme.colorScheme.tertiary
} else {
MaterialTheme.colorScheme.surface
}
),
) {
Column(
modifier = Modifier
.width(42.dp)
.height(42.dp)
.padding(8.dp)
.fillMaxSize(), // Fill the available size in the Column
verticalArrangement = Arrangement.Center, // Center vertically
horizontalAlignment = Alignment.CenterHorizontally // Center horizontally
) {
Text(
text = date.date.toFormattedDateShortString(),
style = MaterialTheme.typography.titleMedium,
fontWeight = if (date.isSelected) {
FontWeight.Medium
} else {
FontWeight.Normal
}
)
}
}
}
}

@Composable
fun DateHeader(
data: CalendarModel,
onPrevClickListener: (Date) -> Unit,
onNextClickListener: (Date) -> Unit
) {
Row(
modifier = Modifier.padding(vertical = 16.dp),
) {
Text(
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically),
text = if (data.selectedDate.isToday) {
"Today"
} else {
data.selectedDate.date.toFormattedMonthDateString()
},
style = MaterialTheme.typography.displaySmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.tertiary
)
IconButton(onClick = {
onPrevClickListener(data.startDate.date)
}) {
Icon(
imageVector = Icons.Filled.KeyboardArrowLeft,
tint = MaterialTheme.colorScheme.tertiary,
contentDescription = "Back"
)
}
IconButton(onClick = {
onNextClickListener(data.endDate.date)
}) {
Icon(
imageVector = Icons.Filled.KeyboardArrowRight,
tint = MaterialTheme.colorScheme.tertiary,
contentDescription = "Next"
)
}
}
}

Expand Down
Loading

0 comments on commit 1657795

Please sign in to comment.