feat: [#10] composants UI réutilisables (TaskRow, PriorityBadge, DueDateChip, LabelChip, SectionHeader, EmptyState)
This commit is contained in:
@@ -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())
|
||||
}
|
||||
@@ -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 +",
|
||||
)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user