From 4dfc224eb636dcac67f8d38f4b4f6d9ba4fa6823 Mon Sep 17 00:00:00 2001 From: Gato Date: Sat, 6 Jun 2026 06:02:50 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20[#10]=20composants=20UI=20r=C3=A9utilis?= =?UTF-8?q?ables=20(TaskRow,=20PriorityBadge,=20DueDateChip,=20LabelChip,?= =?UTF-8?q?=20SectionHeader,=20EmptyState)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobile/ui/components/DueDateChip.kt | 50 ++++++++ .../mobile/ui/components/EmptyState.kt | 67 ++++++++++ .../planify/mobile/ui/components/LabelChip.kt | 30 +++++ .../mobile/ui/components/PriorityBadge.kt | 34 +++++ .../mobile/ui/components/SectionHeader.kt | 80 ++++++++++++ .../planify/mobile/ui/components/TaskRow.kt | 119 ++++++++++++++++++ 6 files changed, 380 insertions(+) create mode 100644 app/src/main/java/com/planify/mobile/ui/components/DueDateChip.kt create mode 100644 app/src/main/java/com/planify/mobile/ui/components/EmptyState.kt create mode 100644 app/src/main/java/com/planify/mobile/ui/components/LabelChip.kt create mode 100644 app/src/main/java/com/planify/mobile/ui/components/PriorityBadge.kt create mode 100644 app/src/main/java/com/planify/mobile/ui/components/SectionHeader.kt create mode 100644 app/src/main/java/com/planify/mobile/ui/components/TaskRow.kt diff --git a/app/src/main/java/com/planify/mobile/ui/components/DueDateChip.kt b/app/src/main/java/com/planify/mobile/ui/components/DueDateChip.kt new file mode 100644 index 0000000..8221f69 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/components/DueDateChip.kt @@ -0,0 +1,50 @@ +package com.planify.mobile.ui.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.CalendarToday +import androidx.compose.material3.Icon +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.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +@Composable +fun DueDateChip(dateIso: String, modifier: Modifier = Modifier) { + val date = runCatching { LocalDate.parse(dateIso.take(10)) }.getOrNull() ?: return + val today = LocalDate.now() + val overdue = date.isBefore(today) + val color = if (overdue) Color(0xFFE53935) else MaterialTheme.colorScheme.onSurfaceVariant + + val label = when (date) { + today -> "Aujourd'hui" + today.plusDays(1) -> "Demain" + else -> date.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)) + } + + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Outlined.CalendarToday, + contentDescription = null, + tint = color, + modifier = Modifier.width(14.dp) + ) + Spacer(Modifier.width(2.dp)) + Text(text = label, style = MaterialTheme.typography.labelSmall, color = color) + } +} + +@Preview +@Composable +private fun DueDateChipPreview() { + DueDateChip(dateIso = LocalDate.now().toString()) +} diff --git a/app/src/main/java/com/planify/mobile/ui/components/EmptyState.kt b/app/src/main/java/com/planify/mobile/ui/components/EmptyState.kt new file mode 100644 index 0000000..77f70ca --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/components/EmptyState.kt @@ -0,0 +1,67 @@ +package com.planify.mobile.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.CheckCircle +import androidx.compose.material3.Icon +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.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun EmptyState( + icon: ImageVector, + title: String, + subtitle: String? = null, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + ) + Spacer(Modifier.height(16.dp)) + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + if (subtitle != null) { + Spacer(Modifier.height(8.dp)) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun EmptyStatePreview() { + EmptyState( + icon = Icons.Outlined.CheckCircle, + title = "Aucune tâche", + subtitle = "Créez votre première tâche avec le bouton +", + ) +} diff --git a/app/src/main/java/com/planify/mobile/ui/components/LabelChip.kt b/app/src/main/java/com/planify/mobile/ui/components/LabelChip.kt new file mode 100644 index 0000000..d52b8ef --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/components/LabelChip.kt @@ -0,0 +1,30 @@ +package com.planify.mobile.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun LabelChip(name: String, color: Color, modifier: Modifier = Modifier) { + Text( + text = name, + style = MaterialTheme.typography.labelSmall, + color = color, + modifier = modifier + .background(color.copy(alpha = 0.12f), RoundedCornerShape(4.dp)) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) +} + +@Preview +@Composable +private fun LabelChipPreview() { + LabelChip(name = "android", color = Color(0xFF1E88E5)) +} diff --git a/app/src/main/java/com/planify/mobile/ui/components/PriorityBadge.kt b/app/src/main/java/com/planify/mobile/ui/components/PriorityBadge.kt new file mode 100644 index 0000000..67a7cd9 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/components/PriorityBadge.kt @@ -0,0 +1,34 @@ +package com.planify.mobile.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +val priorityColor = mapOf( + 1 to Color(0xFFE53935), + 2 to Color(0xFFFB8C00), + 3 to Color(0xFF1E88E5), + 4 to Color(0xFF9E9E9E), +) + +@Composable +fun PriorityBadge(priority: Int, modifier: Modifier = Modifier) { + val color = priorityColor[priority] ?: priorityColor[4]!! + Box( + modifier = modifier + .size(12.dp) + .background(color, CircleShape) + ) +} + +@Preview +@Composable +private fun PriorityBadgePreview() { + PriorityBadge(priority = 1) +} diff --git a/app/src/main/java/com/planify/mobile/ui/components/SectionHeader.kt b/app/src/main/java/com/planify/mobile/ui/components/SectionHeader.kt new file mode 100644 index 0000000..478d70c --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/components/SectionHeader.kt @@ -0,0 +1,80 @@ +package com.planify.mobile.ui.components + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.clickable +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.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ExpandMore +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun SectionHeader( + name: String, + taskCount: Int, + collapsed: Boolean, + onToggleCollapse: () -> Unit, + onAddTask: () -> Unit, + modifier: Modifier = Modifier, +) { + val rotation by animateFloatAsState(if (collapsed) -90f else 0f, label = "collapse") + + Row( + modifier = modifier + .fillMaxWidth() + .clickable { onToggleCollapse() } + .padding(start = 8.dp, end = 4.dp, top = 4.dp, bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.ExpandMore, + contentDescription = if (collapsed) "Déplier" else "Replier", + modifier = Modifier.rotate(rotation), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.width(8.dp)) + Text( + text = name, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.weight(1f), + ) + if (taskCount > 0) { + Text( + text = "$taskCount", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + IconButton(onClick = onAddTask) { + Icon(Icons.Default.Add, contentDescription = "Ajouter une tâche") + } + } +} + +@Preview(showBackground = true) +@Composable +private fun SectionHeaderPreview() { + SectionHeader( + name = "En cours", + taskCount = 3, + collapsed = false, + onToggleCollapse = {}, + onAddTask = {}, + ) +} diff --git a/app/src/main/java/com/planify/mobile/ui/components/TaskRow.kt b/app/src/main/java/com/planify/mobile/ui/components/TaskRow.kt new file mode 100644 index 0000000..83c9750 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/ui/components/TaskRow.kt @@ -0,0 +1,119 @@ +package com.planify.mobile.ui.components + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.planify.mobile.domain.model.Task + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun TaskRow( + task: Task, + onCheckedChange: (Boolean) -> Unit, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val priorityColor = priorityColor[task.priority] ?: Color.Gray + val textColor by animateColorAsState( + if (task.checked) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + else MaterialTheme.colorScheme.onSurface, + label = "textColor", + ) + + Row( + modifier = modifier + .fillMaxWidth() + .combinedClickable(onClick = onClick, onLongClick = {}) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + checked = task.checked, + onCheckedChange = onCheckedChange, + colors = CheckboxDefaults.colors( + checkedColor = priorityColor, + uncheckedColor = priorityColor, + ), + ) + Spacer(Modifier.width(4.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = task.content, + style = MaterialTheme.typography.bodyMedium, + color = textColor, + textDecoration = if (task.checked) TextDecoration.LineThrough else null, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (task.dueDate != null) { + DueDateChip(dateIso = task.dueDate.date) + } + task.labels.take(2).forEach { labelName -> + LabelChip(name = labelName, color = MaterialTheme.colorScheme.primary) + } + } + } + if (task.priority < 4) { + Spacer(Modifier.width(8.dp)) + PriorityBadge(priority = task.priority) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun TaskRowPreview() { + Surface { + Column { + TaskRow( + task = Task( + id = "1", + content = "Implémenter la navigation principale", + projectId = "p1", + priority = 2, + labels = listOf("android", "ui"), + ), + onCheckedChange = {}, + onClick = {}, + ) + Spacer(Modifier.height(1.dp)) + TaskRow( + task = Task( + id = "2", + content = "Tâche terminée", + projectId = "p1", + priority = 4, + checked = true, + ), + onCheckedChange = {}, + onClick = {}, + ) + } + } +}