feat: add UI screens design
This commit is contained in:
@@ -1,64 +1,392 @@
|
||||
import { ScrollView, Text } from "react-native";
|
||||
import { ScrollView, View, Text, StyleSheet, Image } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Redirect } from "expo-router";
|
||||
|
||||
import { COLORS } from "../constants/colors";
|
||||
|
||||
import SectionTitle from "../components/SectionTitle";
|
||||
import AlertItem from "@/components/Alertltem";
|
||||
|
||||
import { useApp } from "../context/AppContext";
|
||||
import { notificationTypeToAlertType } from "../lib/notification-mapper";
|
||||
import type { NotificationType } from "../services/tracking.service";
|
||||
|
||||
type Severity = "info" | "attention" | "important" | "critical";
|
||||
|
||||
const meta: Record<
|
||||
NotificationType,
|
||||
{
|
||||
severity: Severity;
|
||||
icon: keyof typeof Ionicons.glyphMap;
|
||||
color: string;
|
||||
bg: string;
|
||||
badge: string;
|
||||
}
|
||||
> = {
|
||||
ROUTE_START: {
|
||||
severity: "info",
|
||||
icon: "navigate",
|
||||
color: "#3B82F6",
|
||||
bg: "#EFF6FF",
|
||||
badge: "Informativo",
|
||||
},
|
||||
TRUCK_PROXIMITY: {
|
||||
severity: "attention",
|
||||
icon: "time-outline",
|
||||
color: "#F59E0B",
|
||||
bg: "#FEF3C7",
|
||||
badge: "Atención",
|
||||
},
|
||||
TRUCK_ARRIVED: {
|
||||
severity: "important",
|
||||
icon: "notifications",
|
||||
color: "#22C55E",
|
||||
bg: "#ECFDF5",
|
||||
badge: "Importante",
|
||||
},
|
||||
ROUTE_COMPLETED: {
|
||||
severity: "info",
|
||||
icon: "checkmark-done",
|
||||
color: "#6B7280",
|
||||
bg: "#F3F4F6",
|
||||
badge: "Informativo",
|
||||
},
|
||||
DELAY: {
|
||||
severity: "critical",
|
||||
icon: "warning-outline",
|
||||
color: "#EF4444",
|
||||
bg: "#FEF2F2",
|
||||
badge: "Crítico",
|
||||
},
|
||||
MECHANICAL_FAILURE: {
|
||||
severity: "critical",
|
||||
icon: "warning",
|
||||
color: "#EF4444",
|
||||
bg: "#FEF2F2",
|
||||
badge: "Crítico",
|
||||
},
|
||||
};
|
||||
|
||||
const STATUS_LABEL: Record<string, { label: string; color: string }> = {
|
||||
EN_RUTA: { label: "ACTIVA", color: "#22C55E" },
|
||||
DETENIDO: { label: "DETENIDO", color: "#F59E0B" },
|
||||
FALLA: { label: "FALLA", color: "#EF4444" },
|
||||
FINALIZADO: { label: "FINALIZADO", color: "#6B7280" },
|
||||
ESPERA: { label: "EN ESPERA", color: "#6B7280" },
|
||||
};
|
||||
|
||||
export default function AlertsScreen() {
|
||||
const { user, notifications } = useApp();
|
||||
const { user, notifications, route } = useApp();
|
||||
|
||||
if (!user) {
|
||||
return <Redirect href="/login" />;
|
||||
}
|
||||
if (!user) return <Redirect href="/login" />;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: COLORS.background,
|
||||
}}
|
||||
const statusInfo =
|
||||
STATUS_LABEL[route?.status ?? "ESPERA"] ?? STATUS_LABEL.ESPERA;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={{ flex: 1, backgroundColor: COLORS.background }}
|
||||
edges={["top"]}
|
||||
>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingBottom: 130 }}
|
||||
>
|
||||
{/* Hero con título + count + camion image en banner azul */}
|
||||
<View style={styles.heroSection}>
|
||||
<View style={styles.heroTextWrap}>
|
||||
<Text style={styles.headerTitle}>Alertas</Text>
|
||||
<Text style={styles.headerCount}>
|
||||
{notifications.length}{" "}
|
||||
{notifications.length === 1
|
||||
? "notificación"
|
||||
: "notificaciones"}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.heroBanner}>
|
||||
<Image
|
||||
source={require("../../assets/illustrations/truck-city.png")}
|
||||
style={styles.truckImage}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Estado de ruta */}
|
||||
<View
|
||||
style={[
|
||||
styles.statusBanner,
|
||||
{ backgroundColor: `${statusInfo.color}15` },
|
||||
]}
|
||||
>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
padding: 20,
|
||||
paddingBottom: 120,
|
||||
}}
|
||||
>
|
||||
<SectionTitle title="Alertas" />
|
||||
<View
|
||||
style={[
|
||||
styles.statusIcon,
|
||||
{ backgroundColor: statusInfo.color },
|
||||
]}
|
||||
>
|
||||
<Ionicons name="bus-outline" size={20} color="#FFFFFF" />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={[styles.statusTitle, { color: statusInfo.color }]}>
|
||||
ESTADO DE RUTA: {statusInfo.label}
|
||||
</Text>
|
||||
<Text style={styles.statusBody}>
|
||||
{statusInfo.label === "ACTIVA"
|
||||
? "El servicio de recolección sigue en curso."
|
||||
: "Sin servicio activo en este momento."}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
color: "#6B7280",
|
||||
fontSize: 14,
|
||||
marginBottom: 24,
|
||||
marginLeft: 12,
|
||||
}}
|
||||
>
|
||||
{notifications.length > 0
|
||||
? `${notifications.length} notificaciones`
|
||||
: "Aún no hay alertas. Vuelve al inicio y desliza para actualizar."}
|
||||
</Text>
|
||||
{/* Timeline de notificaciones */}
|
||||
{notifications.length === 0 ? (
|
||||
<View style={styles.emptyBox}>
|
||||
<Ionicons name="leaf-outline" size={40} color="#9CA3AF" />
|
||||
<Text style={styles.emptyText}>
|
||||
Aún no hay alertas. Las recibirás cuando el camión avance en
|
||||
tu ruta.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={{ paddingHorizontal: 20, marginTop: 12 }}>
|
||||
{notifications.map((n, idx) => {
|
||||
const m = meta[n.type];
|
||||
const isLast = idx === notifications.length - 1;
|
||||
const timeStr = new Date(n.createdAt).toLocaleTimeString(
|
||||
"es-MX",
|
||||
{ hour: "2-digit", minute: "2-digit" },
|
||||
);
|
||||
return (
|
||||
<View key={n.id} style={styles.timelineRow}>
|
||||
{/* Columna izquierda: dot + línea */}
|
||||
<View style={styles.timelineLeft}>
|
||||
<View
|
||||
style={[
|
||||
styles.timelineDot,
|
||||
{ backgroundColor: m.color },
|
||||
]}
|
||||
>
|
||||
<Ionicons name={m.icon} size={14} color="#FFFFFF" />
|
||||
</View>
|
||||
{!isLast && (
|
||||
<View
|
||||
style={[
|
||||
styles.timelineLine,
|
||||
{ backgroundColor: `${m.color}50` },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<Text style={styles.timelineTime}>{timeStr}</Text>
|
||||
</View>
|
||||
|
||||
{notifications.map((n) => (
|
||||
<AlertItem
|
||||
key={n.id}
|
||||
title={n.title}
|
||||
description={n.body}
|
||||
time={new Date(n.createdAt).toLocaleTimeString("es-MX", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
type={notificationTypeToAlertType(n.type)}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
{/* Card */}
|
||||
<View
|
||||
style={[
|
||||
styles.notifCard,
|
||||
{ backgroundColor: m.bg },
|
||||
]}
|
||||
>
|
||||
<View style={styles.notifHeader}>
|
||||
<Text style={styles.notifTitle}>{n.title}</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.badge,
|
||||
{ backgroundColor: `${m.color}25` },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.badgeText, { color: m.color }]}>
|
||||
{m.badge}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.notifBody}>{n.body}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Consejo del día */}
|
||||
<View style={styles.tipCard}>
|
||||
<View style={styles.tipIcon}>
|
||||
<Ionicons name="leaf" size={20} color="#0E8A61" />
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={styles.tipTitle}>Consejo del día</Text>
|
||||
<Text style={styles.tipBody}>
|
||||
Separa correctamente tus residuos. Tu acción hace la diferencia.
|
||||
</Text>
|
||||
</View>
|
||||
<Image
|
||||
source={require("../../assets/illustrations/bins-cute.png")}
|
||||
style={styles.tipImage}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
heroSection: {
|
||||
backgroundColor: "#DBEAFE",
|
||||
paddingBottom: 0,
|
||||
marginBottom: 20,
|
||||
},
|
||||
heroTextWrap: {
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
heroBanner: {
|
||||
height: 150,
|
||||
width: "100%",
|
||||
backgroundColor: "#DBEAFE",
|
||||
overflow: "hidden",
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 32,
|
||||
fontWeight: "800",
|
||||
color: "#0F172A",
|
||||
},
|
||||
headerCount: {
|
||||
fontSize: 14,
|
||||
color: "#6B7280",
|
||||
marginTop: 2,
|
||||
},
|
||||
truckImage: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
statusBanner: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginHorizontal: 20,
|
||||
padding: 14,
|
||||
borderRadius: 14,
|
||||
marginBottom: 4,
|
||||
},
|
||||
statusIcon: {
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 19,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 12,
|
||||
},
|
||||
statusTitle: {
|
||||
fontSize: 13,
|
||||
fontWeight: "800",
|
||||
},
|
||||
statusBody: {
|
||||
fontSize: 12,
|
||||
color: "#374151",
|
||||
marginTop: 2,
|
||||
},
|
||||
emptyBox: {
|
||||
alignItems: "center",
|
||||
padding: 40,
|
||||
marginHorizontal: 20,
|
||||
marginTop: 16,
|
||||
backgroundColor: "#FFFFFF",
|
||||
borderRadius: 14,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 13,
|
||||
color: "#6B7280",
|
||||
textAlign: "center",
|
||||
marginTop: 10,
|
||||
},
|
||||
timelineRow: {
|
||||
flexDirection: "row",
|
||||
marginBottom: 14,
|
||||
},
|
||||
timelineLeft: {
|
||||
width: 56,
|
||||
alignItems: "center",
|
||||
},
|
||||
timelineDot: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
timelineLine: {
|
||||
flex: 1,
|
||||
width: 2,
|
||||
marginTop: 2,
|
||||
marginBottom: 2,
|
||||
},
|
||||
timelineTime: {
|
||||
fontSize: 10,
|
||||
color: "#9CA3AF",
|
||||
marginTop: 4,
|
||||
},
|
||||
notifCard: {
|
||||
flex: 1,
|
||||
borderRadius: 14,
|
||||
padding: 14,
|
||||
marginLeft: 4,
|
||||
},
|
||||
notifHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
marginBottom: 6,
|
||||
},
|
||||
notifTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: "800",
|
||||
color: "#0F172A",
|
||||
flex: 1,
|
||||
paddingRight: 8,
|
||||
},
|
||||
badge: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 8,
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 10,
|
||||
fontWeight: "700",
|
||||
},
|
||||
notifBody: {
|
||||
fontSize: 13,
|
||||
color: "#374151",
|
||||
lineHeight: 18,
|
||||
},
|
||||
tipCard: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#ECFDF5",
|
||||
borderRadius: 16,
|
||||
padding: 14,
|
||||
marginHorizontal: 20,
|
||||
marginTop: 16,
|
||||
},
|
||||
tipIcon: {
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 21,
|
||||
backgroundColor: "#FFFFFF",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 12,
|
||||
},
|
||||
tipTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: "800",
|
||||
color: "#065F46",
|
||||
marginBottom: 4,
|
||||
},
|
||||
tipBody: {
|
||||
fontSize: 12,
|
||||
color: "#065F46",
|
||||
lineHeight: 16,
|
||||
},
|
||||
tipImage: {
|
||||
width: 70,
|
||||
height: 70,
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user