diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/OutLinedButton.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/OutLinedButton.kt new file mode 100644 index 00000000..7edce2fc --- /dev/null +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/component/OutLinedButton.kt @@ -0,0 +1,45 @@ +package com.unifest.android.core.designsystem.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +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.unit.dp +import com.unifest.android.core.designsystem.ComponentPreview + +@Composable +fun UnifestOutlinedButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + borderColor: Color = Color(0xFFf5678E), + contentColor: Color = Color(0xFFf5678E), + enabled: Boolean = true, + content: @Composable RowScope.() -> Unit, +) { + OutlinedButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = contentColor, + ), + border = BorderStroke(1.dp, borderColor), + content = content, + ) +} + +@ComponentPreview +@Composable +fun UnifestOutlinedButtonPreview() { + UnifestOutlinedButton( + onClick = {}, + ) { + Text("Outlined Button") + } +} diff --git a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt index 9cad6dda..24b2e456 100644 --- a/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt +++ b/core/designsystem/src/main/kotlin/com/unifest/android/core/designsystem/theme/Font.kt @@ -50,6 +50,11 @@ val Title5 = TextStyle( fontWeight = FontWeight.SemiBold, fontSize = 13.sp, ) +val BoothTitle0 = TextStyle( + fontFamily = pretendardFamily, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, +) val BoothTitle1 = TextStyle( fontFamily = pretendardFamily, @@ -116,3 +121,15 @@ val Content4 = TextStyle( fontWeight = FontWeight.Bold, fontSize = 12.sp, ) + +val Content5 = TextStyle( + fontFamily = pretendardFamily, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, +) + +val Content6 = TextStyle( + fontFamily = pretendardFamily, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, +) diff --git a/core/designsystem/src/main/res/drawable/calender_bottom.xml b/core/designsystem/src/main/res/drawable/calender_bottom.xml new file mode 100644 index 00000000..50c22b02 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/calender_bottom.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_calender_down.xml b/core/designsystem/src/main/res/drawable/ic_calender_down.xml new file mode 100644 index 00000000..611e4029 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_calender_down.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_calender_up.xml b/core/designsystem/src/main/res/drawable/ic_calender_up.xml new file mode 100644 index 00000000..f1681b5d --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_calender_up.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_location.xml b/core/designsystem/src/main/res/drawable/ic_location.xml new file mode 100644 index 00000000..102279a3 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_location.xml @@ -0,0 +1,13 @@ + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_schedule.xml b/core/designsystem/src/main/res/drawable/ic_schedule.xml new file mode 100644 index 00000000..b50b8a03 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_schedule.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml index 5505387e..294b8cad 100644 --- a/core/designsystem/src/main/res/values/strings.xml +++ b/core/designsystem/src/main/res/values/strings.xml @@ -24,6 +24,16 @@ 경상도 + + 오늘의 축제 일정 + 오늘은 축제가 열리는 학교가 없어요 + 다가오는 축제 일정 + 관심 축제 추가하기 + 관심 축제로 추가 + 축제 일정 없음 + 오늘은 축제가 열리는 학교가 없어요 + + Week mode diff --git a/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/FestivalEventEntity.kt b/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/FestivalEventEntity.kt new file mode 100644 index 00000000..592174e3 --- /dev/null +++ b/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/FestivalEventEntity.kt @@ -0,0 +1,12 @@ +package com.unifest.android.core.domain.entity + +import androidx.compose.runtime.Stable + +@Stable +data class FestivalEventEntity( + val id: Int, + val date: String, + val name: String, + val location: String, + val celebrityImages: List, +) diff --git a/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/IncomingFestivalEventEntity.kt b/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/IncomingFestivalEventEntity.kt new file mode 100644 index 00000000..bdd204d3 --- /dev/null +++ b/core/domain/src/main/kotlin/com/unifest/android/core/domain/entity/IncomingFestivalEventEntity.kt @@ -0,0 +1,11 @@ +package com.unifest.android.core.domain.entity + +import androidx.compose.runtime.Stable + +@Stable +data class IncomingFestivalEventEntity( + val imageRes: Int, + val name: String, + val dates: String, + val location: String, +) diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/Calendar.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/Calendar.kt index fbeb2ca1..586d902a 100644 --- a/feature/home/src/main/kotlin/com/unifest/android/feature/home/Calendar.kt +++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/Calendar.kt @@ -2,27 +2,24 @@ package com.unifest.android.feature.home import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio 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.requiredHeight import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -30,11 +27,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.paint import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -55,34 +53,31 @@ import com.kizitonwose.calendar.core.nextMonth import com.kizitonwose.calendar.core.previousMonth import com.kizitonwose.calendar.core.yearMonth import com.unifest.android.core.designsystem.R +import com.unifest.android.core.designsystem.theme.BoothTitle0 import com.unifest.android.core.designsystem.theme.UnifestTheme import kotlinx.coroutines.launch import java.time.DayOfWeek import java.time.LocalDate -import java.time.YearMonth +import java.time.Month @Composable fun Calendar(adjacentMonths: Long = 500) { val currentDate = remember { LocalDate.now() } - val currentMonth = remember(currentDate) { currentDate.yearMonth } - val startMonth = remember(currentDate) { currentMonth.minusMonths(adjacentMonths) } - val endMonth = remember(currentDate) { currentMonth.plusMonths(adjacentMonths) } - val selections = remember { mutableStateListOf() } + val currentYearMonth = remember(currentDate) { currentDate.yearMonth } + val startMonth = remember(currentDate) { currentYearMonth.minusMonths(adjacentMonths) } + val endMonth = remember(currentDate) { currentYearMonth.plusMonths(adjacentMonths) } + val selectedDate = remember { mutableStateOf(LocalDate.now()) } val daysOfWeek = remember { daysOfWeek() } - var isWeekMode by remember { mutableStateOf(false) } - var isAnimating by remember { mutableStateOf(false) } - val coroutineScope = rememberCoroutineScope() Column( modifier = Modifier - .fillMaxSize() .background(Color.White), ) { val monthState = rememberCalendarState( startMonth = startMonth, endMonth = endMonth, - firstVisibleMonth = currentMonth, + firstVisibleMonth = currentYearMonth, firstDayOfWeek = daysOfWeek.first(), ) val weekState = rememberWeekCalendarState( @@ -91,12 +86,13 @@ fun Calendar(adjacentMonths: Long = 500) { firstVisibleWeekDate = currentDate, firstDayOfWeek = daysOfWeek.first(), ) - CalendarTitle( + + MonthAndWeekCalendarTitle( isWeekMode = isWeekMode, monthState = monthState, weekState = weekState, - isAnimating = isAnimating, ) + CalendarHeader(daysOfWeek = daysOfWeek) AnimatedVisibility(visible = !isWeekMode) { HorizontalCalendar( @@ -105,14 +101,10 @@ fun Calendar(adjacentMonths: Long = 500) { val isSelectable = day.position == DayPosition.MonthDate Day( day.date, - isSelected = isSelectable && selections.contains(day.date), + isSelected = isSelectable && selectedDate.value == day.date, isSelectable = isSelectable, ) { clicked -> - if (selections.contains(clicked)) { - selections.remove(clicked) - } else { - selections.add(clicked) - } + selectedDate.value = clicked } }, ) @@ -124,105 +116,49 @@ fun Calendar(adjacentMonths: Long = 500) { val isSelectable = day.position == WeekDayPosition.RangeDate Day( day.date, - isSelected = isSelectable && selections.contains(day.date), + isSelected = isSelectable && selectedDate.value == day.date, isSelectable = isSelectable, ) { clicked -> - if (selections.contains(clicked)) { - selections.remove(clicked) - } else { - selections.add(clicked) - } + selectedDate.value = clicked } }, ) } - Spacer(modifier = Modifier.weight(1f)) - WeekModeToggle( - modifier = Modifier.align(Alignment.CenterHorizontally), + + ModeToggleButton( isWeekMode = isWeekMode, - ) { weekMode -> - isAnimating = true - isWeekMode = weekMode - coroutineScope.launch { - if (weekMode) { - val targetDate = monthState.firstVisibleMonth.weekDays.last().last().date - weekState.scrollToWeek(targetDate) - weekState.animateScrollToWeek(targetDate) // Trigger a layout pass for title update - } else { - val targetMonth = weekState.firstVisibleWeek.days.first().date.yearMonth - monthState.scrollToMonth(targetMonth) - monthState.animateScrollToMonth(targetMonth) // Trigger a layout pass for title update - } - isAnimating = false - } - } + onModeChange = { isWeekMode = it }, + ) } } @Composable -fun MonthAndWeekCalendarTitle( +fun ModeToggleButton( + modifier: Modifier = Modifier, isWeekMode: Boolean, - currentMonth: YearMonth, - monthState: CalendarState, - weekState: WeekCalendarState, + onModeChange: (Boolean) -> Unit, ) { - val coroutineScope = rememberCoroutineScope() - SimpleCalendarTitle( - modifier = Modifier.padding(vertical = 10.dp, horizontal = 8.dp), - currentMonth = currentMonth, - goToPrevious = { - coroutineScope.launch { - if (isWeekMode) { - val targetDate = weekState.firstVisibleWeek.days.first().date.minusDays(1) - weekState.animateScrollToWeek(targetDate) - } else { - val targetMonth = monthState.firstVisibleMonth.yearMonth.previousMonth - monthState.animateScrollToMonth(targetMonth) - } - } - }, - goToNext = { - coroutineScope.launch { - if (isWeekMode) { - val targetDate = weekState.firstVisibleWeek.days.last().date.plusDays(1) - weekState.animateScrollToWeek(targetDate) - } else { - val targetMonth = monthState.firstVisibleMonth.yearMonth.nextMonth - monthState.animateScrollToMonth(targetMonth) - } - } - }, - ) -} + val icon = if (isWeekMode) painterResource(id = R.drawable.ic_calender_down) else painterResource(id = R.drawable.ic_calender_up) + val contentDescription = if (isWeekMode) "Month" else "Week" + val backgroundPainter = painterResource(id = R.drawable.calender_bottom) -@Composable -fun SimpleCalendarTitle( - modifier: Modifier, - currentMonth: YearMonth, - goToPrevious: () -> Unit, - goToNext: () -> Unit, -) { - Row( - modifier = modifier.height(40.dp), - verticalAlignment = Alignment.CenterVertically, + Box( + modifier = Modifier + .fillMaxWidth() + .requiredHeight(40.dp) + .paint(painter = backgroundPainter, contentScale = ContentScale.FillBounds) + .then(modifier), + contentAlignment = Alignment.Center, ) { - CalendarNavigationIcon( - icon = painterResource(id = R.drawable.ic_chevron_left), - contentDescription = "Previous", - onClick = goToPrevious, - ) - Text( - modifier = Modifier.weight(1f), - text = currentMonth.displayText(), - fontSize = 22.sp, - textAlign = TextAlign.Center, - fontWeight = FontWeight.Medium, - ) - CalendarNavigationIcon( - icon = painterResource(id = R.drawable.ic_chevron_right), - contentDescription = "Next", - onClick = goToNext, - ) + IconButton( + onClick = { onModeChange(!isWeekMode) }, + ) { + Icon( + painter = icon, + contentDescription = contentDescription, + tint = Color(0xFFD9D9D9), + ) + } } } @@ -245,32 +181,79 @@ private fun CalendarNavigationIcon( .align(Alignment.Center), painter = icon, contentDescription = contentDescription, + tint = Color.Gray, ) } @Composable -private fun CalendarTitle( +fun MonthAndWeekCalendarTitle( isWeekMode: Boolean, monthState: CalendarState, weekState: WeekCalendarState, - isAnimating: Boolean, ) { val visibleMonth = rememberFirstVisibleMonthAfterScroll(monthState) - val visibleWeek = rememberFirstVisibleWeekAfterScroll(weekState) - val visibleWeekMonth = visibleWeek.days.first().date.yearMonth - // Track animation state to prevent updating the title too early before - // the correct value is available (after the animation). - val currentMonth = if (isWeekMode) { - if (isAnimating) visibleMonth.yearMonth else visibleWeekMonth - } else { - if (isAnimating) visibleWeekMonth else visibleMonth.yearMonth + val currentMonth = visibleMonth.yearMonth.month + + val coroutineScope = rememberCoroutineScope() + if (!isWeekMode) { + SimpleCalendarTitle( + modifier = Modifier.padding(20.dp), + currentMonth = currentMonth, + goToPrevious = { + coroutineScope.launch { + if (isWeekMode) { + val targetDate = weekState.firstVisibleWeek.days.first().date.minusDays(1) + weekState.animateScrollToWeek(targetDate) + } else { + val targetMonth = monthState.firstVisibleMonth.yearMonth.previousMonth + monthState.animateScrollToMonth(targetMonth) + } + } + }, + goToNext = { + coroutineScope.launch { + if (isWeekMode) { + val targetDate = weekState.firstVisibleWeek.days.last().date.plusDays(1) + weekState.animateScrollToWeek(targetDate) + } else { + val targetMonth = monthState.firstVisibleMonth.yearMonth.nextMonth + monthState.animateScrollToMonth(targetMonth) + } + } + }, + ) + } +} + +@Composable +fun SimpleCalendarTitle( + // 실제로 달력의 상단에 현재 월을 표시하고, 이전/다음 월로 이동할 수 있는 화살표 아이콘을 제공하는 UI 컴포넌트 + modifier: Modifier, + currentMonth: Month, + goToPrevious: () -> Unit, + goToNext: () -> Unit, +) { + Row( + modifier = modifier.height(40.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = currentMonth.displayText(), + style = BoothTitle0, + textAlign = TextAlign.Start, + ) + CalendarNavigationIcon( + icon = painterResource(id = R.drawable.ic_chevron_left), + contentDescription = "Previous", + onClick = goToPrevious, + ) + CalendarNavigationIcon( + icon = painterResource(id = R.drawable.ic_chevron_right), + contentDescription = "Next", + onClick = goToNext, + ) } - MonthAndWeekCalendarTitle( - isWeekMode = isWeekMode, - currentMonth = currentMonth, - monthState = monthState, - weekState = weekState, - ) } @Composable @@ -285,7 +268,8 @@ fun CalendarHeader(daysOfWeek: List) { textAlign = TextAlign.Center, fontSize = 15.sp, text = dayOfWeek.displayText(), - fontWeight = FontWeight.Medium, + fontWeight = FontWeight.Bold, + color = Color.Gray, ) } } @@ -298,54 +282,41 @@ fun Day( isSelectable: Boolean, onClick: (LocalDate) -> Unit, ) { - Box( - modifier = Modifier - .aspectRatio(1f) // This is important for square-sizing! - .padding(6.dp) - .clip(CircleShape) - .background(color = if (isSelected) colorResource(R.color.example_1_selection_color) else Color.Transparent) - .clickable( - enabled = isSelectable, - showRipple = !isSelected, - onClick = { onClick(day) }, - ), - contentAlignment = Alignment.Center, - ) { - val textColor = when { - isSelected -> Color.White - isSelectable -> Color.Unspecified - else -> colorResource(R.color.inactive_text_color) - } - Text( - text = day.dayOfMonth.toString(), - color = textColor, - fontSize = 14.sp, - ) - } -} + val currentDate = LocalDate.now() + val isToday = day == currentDate -@Composable -fun WeekModeToggle( - modifier: Modifier, - isWeekMode: Boolean, - weekModeToggled: (isWeekMode: Boolean) -> Unit, -) { - // We want the entire content to be clickable, not just the checkbox. - Row( - modifier = modifier - .padding(10.dp) - .clip(MaterialTheme.shapes.small) - .clickable { weekModeToggled(!isWeekMode) } - .padding(10.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally), - ) { - Checkbox( - checked = isWeekMode, - onCheckedChange = null, // Check is handled by parent. - colors = CheckboxDefaults.colors(checkedColor = colorResource(R.color.example_1_selection_color)), - ) - Text(text = stringResource(R.string.week_mode)) + Column { + Box( + modifier = Modifier + .aspectRatio(1f) // This is important for square-sizing! + .padding(12.dp) + .clip(CircleShape) + .background(color = if (isSelected) Color(0xFFF5687E) else Color.Transparent) + .then( + if (day == currentDate) { + Modifier.border(2.dp, Color(0xFFF5687E), CircleShape) + } else Modifier, + ) + .clickable( + enabled = isSelectable, + showRipple = !isSelected, + onClick = { onClick(day) }, + ), + contentAlignment = Alignment.Center, + ) { + val textColor = when { + isSelected -> Color.White + isToday -> Color(0xFFF5687E) + isSelectable -> Color.Unspecified + else -> colorResource(R.color.inactive_text_color) + } + Text( + text = day.dayOfMonth.toString(), + color = textColor, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + ) + } } } diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/HomeScreen.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/HomeScreen.kt index 4d26026e..8c3686c8 100644 --- a/feature/home/src/main/kotlin/com/unifest/android/feature/home/HomeScreen.kt +++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/HomeScreen.kt @@ -1,26 +1,77 @@ package com.unifest.android.feature.home +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box 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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Circle +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.core.view.WindowInsetsCompat +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.unifest.android.core.designsystem.R -import com.unifest.android.core.designsystem.component.TopAppBarNavigationType -import com.unifest.android.core.designsystem.component.UnifestTopAppBar +import com.unifest.android.core.designsystem.component.UnifestOutlinedButton +import com.unifest.android.core.designsystem.theme.BoothLocation +import com.unifest.android.core.designsystem.theme.Content4 +import com.unifest.android.core.designsystem.theme.Content5 +import com.unifest.android.core.designsystem.theme.Content6 +import com.unifest.android.core.designsystem.theme.Title2 +import com.unifest.android.core.designsystem.theme.Title3 import com.unifest.android.core.designsystem.theme.UnifestTheme +import com.unifest.android.core.domain.entity.FestivalEventEntity +import com.unifest.android.core.domain.entity.IncomingFestivalEventEntity import com.unifest.android.core.ui.DevicePreview +import com.unifest.android.feature.home.viewmodel.HomeUiState +import com.unifest.android.feature.home.viewmodel.HomeViewModel +import kotlinx.collections.immutable.persistentListOf @Composable internal fun HomeRoute( padding: PaddingValues, onNavigateToIntro: () -> Unit, + viewModel: HomeViewModel = hiltViewModel(), ) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() HomeScreen( padding = padding, + uiState = uiState, onNavigateToIntro = onNavigateToIntro, ) } @@ -28,28 +79,302 @@ internal fun HomeRoute( @Composable internal fun HomeScreen( padding: PaddingValues, + uiState: HomeUiState, @Suppress("unused") onNavigateToIntro: () -> Unit, ) { - Column( + var selectedEventId by remember { mutableIntStateOf(-1) } + val view = LocalView.current + val insets = with(LocalDensity.current) { + WindowInsetsCompat.toWindowInsetsCompat(view.rootWindowInsets, view).getInsets(WindowInsetsCompat.Type.statusBars()).top.toDp() + } + + Column(modifier = Modifier.padding(top = insets)) { + LazyColumn( + modifier = Modifier + .weight(1f) + .padding(bottom = padding.calculateBottomPadding()), + ) { + item { Calendar() } + item { + FestivalScheduleText() + } + if (uiState.festivalEvents.isEmpty()) { + item { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .padding(64.dp), + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_schedule), + contentDescription = "축제 없음", + modifier = Modifier.size(23.dp), + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = stringResource(id = R.string.home_empty_festival_text), + style = Title2, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(9.dp)) + Text( + text = stringResource(id = R.string.home_empty_festival_schedule_description_text), + style = Content6, + textAlign = TextAlign.Center, + color = Color(0xFF848484), + ) + } + } + } + } else { + itemsIndexed(uiState.festivalEvents) { index, event -> + Column { + Spacer(modifier = Modifier.height(16.dp)) + FestivalScheduleItem(event, selectedEventId) { eventId -> + selectedEventId = if (selectedEventId == eventId) -1 else eventId + } + } + if (index < uiState.festivalEvents.size - 1) { + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider( + color = Color(0xFFDFDFDF), + modifier = Modifier.padding(horizontal = 20.dp), + thickness = 1.dp, + ) + } + } + } + item { + UnifestOutlinedButton( + onClick = { onNavigateToIntro() }, + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + contentColor = Color(0xFF585858), + borderColor = Color(0xFFD2D2D2), + ) { + Text( + text = stringResource(id = R.string.home_add_interest_festival_button), + style = BoothLocation, + ) + } + } + item { + IncomingFestivalText() + } + items(uiState.incomingEvents) { event -> + IncomingFestivalCard(event) + Spacer(modifier = Modifier.height(8.dp)) + } + } + } +} + +@Composable +fun FestivalScheduleText() { + Text( + text = stringResource(id = R.string.home_festival_schedule_text), + style = Title3, + modifier = Modifier.padding(start = 20.dp, top = 20.dp), + ) +} + +@Composable +fun FestivalScheduleItem( + event: FestivalEventEntity, + selectedEventId: Int, + onEventClick: (Int) -> Unit, +) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onEventClick(event.id) } + .padding(start = 20.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .width(3.dp) + .height(72.dp) + .background(Color(0xFF1FC0BA)) + .align(Alignment.CenterVertically), + ) + Spacer(modifier = Modifier.width(8.dp)) + Column { + Text( + text = event.date, + style = Content4, + color = Color(0xFFC0C0C0), + ) + Spacer(modifier = Modifier.height(5.dp)) + Text( + text = event.name, + style = Title2, + ) + Spacer(modifier = Modifier.height(7.dp)) + Row { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_location), + contentDescription = "Location Icon", + modifier = Modifier + .size(10.dp) + .align(Alignment.CenterVertically), + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(5.dp)) + Text( + text = event.location, + style = Content5, + color = Color(0xFF848484), + ) + } + } + Spacer(modifier = Modifier.width(39.dp)) + LazyRow { + items(event.celebrityImages) { _ -> + Icon( + imageVector = Icons.Default.Circle, + contentDescription = "Celebrity", + tint = Color(0xFFDFDFDF), + modifier = Modifier + .size(60.dp) + .padding(horizontal = 5.dp), + ) + } + } + } + AnimatedVisibility(visible = selectedEventId == event.id) { + UnifestOutlinedButton( + onClick = { /*관심 축제 추가하기 버튼*/ }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, start = 20.dp, end = 20.dp), + ) { + Text( + text = stringResource(id = R.string.home_add_interest_festival_in_item_button), + style = BoothLocation, + ) + } + } + } +} + +@Composable +fun IncomingFestivalText() { + Text( + text = stringResource(id = R.string.home_incoming_festival_text), + modifier = Modifier.padding(start = 20.dp, bottom = 16.dp), + style = Title3, + ) +} + +@Composable +fun IncomingFestivalCard(event: IncomingFestivalEventEntity) { + Card( modifier = Modifier - .fillMaxSize() - .padding(bottom = padding.calculateBottomPadding()), - horizontalAlignment = Alignment.CenterHorizontally, + .padding(horizontal = 20.dp) + .fillMaxWidth(), + shape = RoundedCornerShape(10.dp), + colors = CardDefaults.cardColors(containerColor = Color.White), + border = BorderStroke(1.dp, Color(0xFFDEDEDE)), ) { - UnifestTopAppBar( - titleRes = R.string.intro_top_app_bar_title, - navigationType = TopAppBarNavigationType.None, - ) - Calendar() + Row( + modifier = Modifier + .padding(20.dp) + .fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + imageVector = ImageVector.vectorResource(id = event.imageRes), + contentDescription = "${event.name} Logo", + modifier = Modifier.size(52.dp), + ) + Spacer(modifier = Modifier.width(10.dp)) + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = event.dates, + style = Content6, + color = Color(0xFF848484), + ) + Spacer(modifier = Modifier.height(5.dp)) + Text( + text = event.name, + style = Content4, + ) + Spacer(modifier = Modifier.height(5.dp)) + Row { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_location), + contentDescription = "Location Icon", + modifier = Modifier + .size(10.dp) + .align(Alignment.CenterVertically), + tint = Color.Unspecified, + ) + Spacer(modifier = Modifier.width(5.dp)) + Text( + text = event.location, + style = Content6, + color = Color(0xFF848484), + ) + } + } + } } } +@OptIn(ExperimentalFoundationApi::class) @DevicePreview @Composable fun HomeScreenPreview() { UnifestTheme { HomeScreen( + uiState = HomeUiState( + festivalEvents = persistentListOf( + FestivalEventEntity( + id = 1, + date = "5/21(화)", + name = "녹색지대 DAY 1", + location = "건국대학교 서울캠퍼스", + celebrityImages = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15), + ), + FestivalEventEntity( + id = 2, + date = "5/21(화)", + name = "녹색지대 DAY 1", + location = "건국대학교 서울캠퍼스", + celebrityImages = listOf(0, 1, 2), + ), + FestivalEventEntity( + id = 3, + date = "5/21(화)", + name = "녹색지대 DAY 1", + location = "건국대학교 서울캠퍼스", + celebrityImages = listOf(0, 1, 2), + ), + ), + incomingEvents = persistentListOf( + IncomingFestivalEventEntity( + imageRes = R.drawable.ic_waiting, + name = "녹색지대", + dates = "05/21(화) - 05/23(목)", + location = "건국대학교 서울캠퍼스", + ), + IncomingFestivalEventEntity( + imageRes = R.drawable.ic_waiting, + name = "녹색지대", + dates = "05/21(화) - 05/23(목)", + location = "건국대학교 서울캠퍼스", + ), + ), + ), + padding = PaddingValues(0.dp), onNavigateToIntro = {}, ) diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeUiState.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeUiState.kt new file mode 100644 index 00000000..b57719f7 --- /dev/null +++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeUiState.kt @@ -0,0 +1,11 @@ +package com.unifest.android.feature.home.viewmodel + +import com.unifest.android.core.domain.entity.FestivalEventEntity +import com.unifest.android.core.domain.entity.IncomingFestivalEventEntity +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +data class HomeUiState( + val incomingEvents: ImmutableList = persistentListOf(), + val festivalEvents: ImmutableList = persistentListOf(), +) diff --git a/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeViewModel.kt b/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeViewModel.kt index be4b416f..676f1b79 100644 --- a/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeViewModel.kt +++ b/feature/home/src/main/kotlin/com/unifest/android/feature/home/viewmodel/HomeViewModel.kt @@ -1,8 +1,63 @@ package com.unifest.android.feature.home.viewmodel import androidx.lifecycle.ViewModel +import com.unifest.android.core.designsystem.R +import com.unifest.android.core.domain.entity.FestivalEventEntity +import com.unifest.android.core.domain.entity.IncomingFestivalEventEntity import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import javax.inject.Inject @HiltViewModel -class HomeViewModel @Inject constructor() : ViewModel() +class HomeViewModel @Inject constructor() : ViewModel() { + private val _uiState = MutableStateFlow(HomeUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + _uiState.update { + it.copy( + incomingEvents = persistentListOf( + IncomingFestivalEventEntity( + imageRes = R.drawable.ic_waiting, + name = "녹색지대", + dates = "05/21(화) - 05/23(목)", + location = "건국대학교 서울캠퍼스", + ), + IncomingFestivalEventEntity( + imageRes = R.drawable.ic_waiting, + name = "녹색지대", + dates = "05/21(화) - 05/23(목)", + location = "건국대학교 서울캠퍼스", + ), + ), + festivalEvents = persistentListOf( + FestivalEventEntity( + id = 1, + date = "5/21(화)", + name = "녹색지대 DAY 1", + location = "건국대학교 서울캠퍼스", + celebrityImages = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15), + ), + FestivalEventEntity( + id = 2, + date = "5/21(화)", + name = "녹색지대 DAY 1", + location = "건국대학교 서울캠퍼스", + celebrityImages = listOf(0, 1, 2), + ), + FestivalEventEntity( + id = 3, + date = "5/21(화)", + name = "녹색지대 DAY 1", + location = "건국대학교 서울캠퍼스", + celebrityImages = listOf(0, 1, 2), + ), + ), + ) + } + } +}