DEV Community

Cover image for Phase 6: Building Support & Chat Features - Action Cards, Conversation Lists, and Ticket History
Md Enayetur Rahman
Md Enayetur Rahman

Posted on

Phase 6: Building Support & Chat Features - Action Cards, Conversation Lists, and Ticket History

The Journey Continues

In Phase 5, I built the Timeline view with tabs, session cards, and a modal for appointment details. Now it's time to build the support system—a hub where patients can chat with support staff, request video consultations, and review their previous support tickets.

Coming from React web development, I was curious: How do you design action-oriented landing pages in mobile? How do chat interfaces work in React Native? What's different about building support systems for mobile vs web? Let me walk you through what I learned, comparing everything to what you already know from React.


What We're Building

In Phase 6, I implemented:

  1. Support Tab with personalized greeting and three action cards
  2. Action cards for Chat, Video Consultation, and Previous Tickets
  3. Chat screen with conversation list and avatar generation
  4. Search functionality for filtering conversations
  5. Previous Tickets screen with status badges and priority indicators
  6. Nested navigation using SupportStackNavigator
  7. Timestamp formatting for chat messages (relative time)
  8. Empty states and alert dialogs for future functionality

Everything uses mock data to simulate real support interactions.


Step 1: Project Structure - Adding Support Types

React (Web) - Typical Support Structure:

my-react-app/
├── src/
│   ├── components/
│   │   ├── Support.js
│   │   ├── ChatList.js
│   │   └── TicketList.js
│   └── data/
│       └── support.js
Enter fullscreen mode Exit fullscreen mode

React Native (What I Built):

physio-care/
├── src/
│   ├── components/
│   │   ├── screens/
│   │   │   ├── SupportTab.tsx
│   │   │   ├── ChatScreen.tsx
│   │   │   └── PreviousTicketsScreen.tsx
│   ├── data/
│   │   └── mockSupport.ts
│   ├── types/
│   │   └── support.ts
│   └── navigation/
│       └── SupportStackNavigator.tsx
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  1. Dedicated stack navigator: Support features get their own navigation stack
  2. Action-first design: Main support screen focuses on quick actions
  3. Chat as list view: Chat UI starts with conversation list, not message threads
  4. Ticket system: Support history tracked separately from appointments

Takeaway: Mobile support UIs prioritize quick actions over comprehensive dashboards. The flow is: action card → specific screen → details.


Step 2: TypeScript Types for Support System

This was crucial for managing support interactions! Let me show you the types I created.

React (Web) - JavaScript Objects:

// support.js
export const conversations = [
  {
    id: "1",
    name: "Dr. Sarah Johnson",
    lastMessage: "Please continue your exercises",
    timestamp: "2024-12-08T14:30:00Z",
    unreadCount: 2,
  },
];

export const tickets = [
  {
    id: "1",
    title: "Scheduling Physiotherapy",
    status: "closed",
    createdAt: "2024-11-15",
  },
];
Enter fullscreen mode Exit fullscreen mode

React Native - TypeScript Types:

// src/types/support.ts
export interface ChatConversation {
  id: string;
  name: string;
  lastMessage: string;
  timestamp: string; // ISO date string
  unreadCount?: number;
  avatar?: string;
}

export interface SupportTicket {
  id: string;
  title: string;
  status: "open" | "closed" | "pending";
  createdAt: string;
  updatedAt: string;
  category: string;
  priority: "low" | "medium" | "high";
}

export type SupportActionType = "chat" | "video" | "tickets";
Enter fullscreen mode Exit fullscreen mode

Benefits of These Types:

  1. Status tracking: Union types ensure valid status values
  2. Optional fields: unreadCount only when there are unread messages
  3. Priority system: Three-level priority for ticket management
  4. Action types: Type-safe action routing in support screen
  5. ISO timestamps: Standard date format for easy parsing

Takeaway: Well-structured support types make it easy to display conversation previews, calculate unread counts, and style status badges consistently.


Step 3: Building the Support Landing Screen

React (Web) - Dashboard Grid:

// Support.js
function Support({ user }) {
  return (
    <div className="support">
      <h2>How can we help you, {user.name}?</h2>
      <div className="support-grid">
        <a href="/chat" className="support-card">
          <span className="icon">💬</span>
          <h3>Chat with Us</h3>
          <p>Get instant help from our support team</p>
        </a>
        <a href="/video-consultation" className="support-card">
          <span className="icon">📹</span>
          <h3>Video Consultation</h3>
          <p>Connect with a physiotherapist virtually</p>
        </a>
        <a href="/tickets" className="support-card">
          <span className="icon">📋</span>
          <h3>Previous Tickets</h3>
          <p>View your support history</p>
        </a>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
/* Support.css */
.support {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.support h2 {
  font-size: 24px;
  margin-bottom: 16px;
}

.support-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
}

.support-card {
  display: flex;
  align-items: center;
  padding: 20px;
  border: 1px solid #e5e5ea;
  border-left: 4px solid #007aff;
  border-radius: 12px;
  text-decoration: none;
  color: inherit;
}

.support-card .icon {
  font-size: 32px;
  margin-right: 16px;
}
Enter fullscreen mode Exit fullscreen mode

React Native - Scroll View with Action Cards:

// src/components/screens/SupportTab.tsx
import React from "react";
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  ScrollView,
} from "react-native";
import { useNavigation } from "@react-navigation/native";
import { useAuth } from "../../context/AuthContext";
import { SupportStackNavigationProp } from "../../types/navigation";
import { mockCenters, mockConsultants } from "../../data/mockAppointments";

export default function SupportTab() {
  const { user } = useAuth();
  const navigation = useNavigation<SupportStackNavigationProp>();

  const userName = user?.name?.split(" ")[0] || "there";

  const handleActionPress = (actionType: "chat" | "video" | "tickets") => {
    switch (actionType) {
      case "chat":
        navigation.navigate("Chat");
        break;
      case "video":
        navigation.navigate("VideoConsultation", {
          center: mockCenters[0],
          consultant: mockConsultants[0],
          sessionType: "online",
        });
        break;
      case "tickets":
        navigation.navigate("PreviousTickets");
        break;
    }
  };

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.greeting}>How can we help you, {userName}?</Text>
        <Text style={styles.subtitle}>
          We're here to support your physiotherapy journey
        </Text>
      </View>

      <View style={styles.actionsContainer}>
        <TouchableOpacity
          style={[styles.actionCard, styles.chatCard]}
          onPress={() => handleActionPress("chat")}
        >
          <View style={styles.actionIcon}>
            <Text style={styles.iconText}>💬</Text>
          </View>
          <View style={styles.actionContent}>
            <Text style={styles.actionTitle}>Chat with Us</Text>
            <Text style={styles.actionDescription}>
              Get instant help from our support team
            </Text>
          </View>
        </TouchableOpacity>

        <TouchableOpacity
          style={[styles.actionCard, styles.videoCard]}
          onPress={() => handleActionPress("video")}
        >
          <View style={styles.actionIcon}>
            <Text style={styles.iconText}>📹</Text>
          </View>
          <View style={styles.actionContent}>
            <Text style={styles.actionTitle}>Video Consultation</Text>
            <Text style={styles.actionDescription}>
              Connect with a physiotherapist virtually
            </Text>
          </View>
        </TouchableOpacity>

        <TouchableOpacity
          style={[styles.actionCard, styles.ticketsCard]}
          onPress={() => handleActionPress("tickets")}
        >
          <View style={styles.actionIcon}>
            <Text style={styles.iconText}>📋</Text>
          </View>
          <View style={styles.actionContent}>
            <Text style={styles.actionTitle}>Previous Tickets</Text>
            <Text style={styles.actionDescription}>
              View your support history and previous queries
            </Text>
          </View>
        </TouchableOpacity>
      </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#f8f9fa",
  },
  header: {
    padding: 20,
    paddingTop: 40,
    backgroundColor: "#fff",
    marginBottom: 16,
  },
  greeting: {
    fontSize: 24,
    fontWeight: "bold",
    color: "#1a1a1a",
    marginBottom: 8,
  },
  subtitle: {
    fontSize: 16,
    color: "#666",
    lineHeight: 22,
  },
  actionsContainer: {
    padding: 16,
  },
  actionCard: {
    flexDirection: "row",
    alignItems: "center",
    backgroundColor: "#fff",
    borderRadius: 12,
    padding: 20,
    marginBottom: 12,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  chatCard: {
    borderLeftWidth: 4,
    borderLeftColor: "#007AFF",
  },
  videoCard: {
    borderLeftWidth: 4,
    borderLeftColor: "#34C759",
  },
  ticketsCard: {
    borderLeftWidth: 4,
    borderLeftColor: "#FF9500",
  },
  actionIcon: {
    width: 50,
    height: 50,
    borderRadius: 25,
    backgroundColor: "#f0f0f0",
    justifyContent: "center",
    alignItems: "center",
    marginRight: 16,
  },
  iconText: {
    fontSize: 24,
  },
  actionContent: {
    flex: 1,
  },
  actionTitle: {
    fontSize: 18,
    fontWeight: "600",
    color: "#1a1a1a",
    marginBottom: 4,
  },
  actionDescription: {
    fontSize: 14,
    color: "#666",
    lineHeight: 20,
  },
});
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  1. ScrollView instead of grid: Better for mobile scrolling
  2. TouchableOpacity for cards: Native press feedback
  3. Navigation over links: Stack navigation for smooth transitions
  4. Elevation/shadow combo: Android elevation + iOS shadow
  5. Personalized greeting: Using first name from auth context
  6. Color-coded borders: Visual distinction between action types

Takeaway: Mobile action cards need larger touch targets (minimum 44px height), clear visual hierarchy, and native press feedback. Color coding helps users quickly identify action types.


Step 4: Building the Chat Conversation List

React (Web) - Simple Message List:

// ChatList.js
function ChatList() {
  const [search, setSearch] = useState("");

  const filtered = conversations.filter(
    (c) =>
      c.name.toLowerCase().includes(search.toLowerCase()) ||
      c.lastMessage.toLowerCase().includes(search.toLowerCase())
  );

  return (
    <div className="chat-list">
      <div className="header">
        <button onClick={() => history.back()}>← Back</button>
        <h2>Chat with Us</h2>
      </div>

      <input
        type="text"
        placeholder="Search conversations..."
        value={search}
        onChange={(e) => setSearch(e.target.value)}
      />

      <div className="conversations">
        {filtered.map((conv) => (
          <div key={conv.id} className="conversation-item">
            <div className="avatar">{conv.name.charAt(0)}</div>
            <div className="content">
              <div className="header">
                <span className="name">{conv.name}</span>
                <span className="time">{formatTime(conv.timestamp)}</span>
              </div>
              <p className="message">{conv.lastMessage}</p>
              {conv.unreadCount > 0 && (
                <span className="badge">{conv.unreadCount}</span>
              )}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

React Native - FlatList with Avatar Generation:

// src/components/screens/ChatScreen.tsx
import React, { useState, useMemo } from "react";
import {
  View,
  Text,
  StyleSheet,
  TextInput,
  FlatList,
  TouchableOpacity,
  Alert,
} from "react-native";
import { useNavigation } from "@react-navigation/native";
import { mockChatConversations } from "../../data/mockSupport";
import { ChatConversation } from "../../types/support";
import { SupportStackNavigationProp } from "../../types/navigation";

export default function ChatScreen() {
  const navigation = useNavigation<SupportStackNavigationProp>();
  const [searchQuery, setSearchQuery] = useState("");

  const filteredConversations = useMemo(() => {
    if (!searchQuery.trim()) {
      return mockChatConversations;
    }

    const query = searchQuery.toLowerCase();
    return mockChatConversations.filter(
      (conversation) =>
        conversation.name.toLowerCase().includes(query) ||
        conversation.lastMessage.toLowerCase().includes(query)
    );
  }, [searchQuery]);

  const handleConversationPress = (conversation: ChatConversation) => {
    Alert.alert(
      "Chat Feature",
      `This would open chat with ${conversation.name}. Chat functionality will be implemented when connecting to backend.`,
      [{ text: "OK" }]
    );
  };

  const formatTimestamp = (timestamp: string) => {
    const date = new Date(timestamp);
    const now = new Date();
    const diffInHours = Math.floor(
      (now.getTime() - date.getTime()) / (1000 * 60 * 60)
    );

    if (diffInHours < 24) {
      return date.toLocaleTimeString("en-US", {
        hour: "numeric",
        minute: "2-digit",
        hour12: true,
      });
    } else {
      return date.toLocaleDateString("en-US", {
        month: "short",
        day: "numeric",
      });
    }
  };

  const renderConversation = ({ item }: { item: ChatConversation }) => (
    <TouchableOpacity
      style={styles.conversationItem}
      onPress={() => handleConversationPress(item)}
    >
      <View style={styles.avatarContainer}>
        <Text style={styles.avatarText}>
          {item.name
            .split(" ")
            .map((n) => n[0])
            .join("")
            .slice(0, 2)}
        </Text>
      </View>

      <View style={styles.conversationContent}>
        <View style={styles.conversationHeader}>
          <Text style={styles.conversationName}>{item.name}</Text>
          <Text style={styles.timestamp}>
            {formatTimestamp(item.timestamp)}
          </Text>
        </View>

        <View style={styles.messageRow}>
          <Text style={styles.lastMessage} numberOfLines={1}>
            {item.lastMessage}
          </Text>
          {item.unreadCount && item.unreadCount > 0 ? (
            <View style={styles.unreadBadge}>
              <Text style={styles.unreadText}>{item.unreadCount}</Text>
            </View>
          ) : null}
        </View>
      </View>
    </TouchableOpacity>
  );

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <TouchableOpacity
          style={styles.backButton}
          onPress={() => navigation.goBack()}
        >
          <Text style={styles.backText}></Text>
        </TouchableOpacity>
        <Text style={styles.headerTitle}>Chat with Us</Text>
      </View>

      <View style={styles.searchContainer}>
        <TextInput
          style={styles.searchInput}
          placeholder="Search conversations..."
          value={searchQuery}
          onChangeText={setSearchQuery}
        />
      </View>

      <FlatList
        data={filteredConversations}
        keyExtractor={(item) => item.id}
        renderItem={renderConversation}
        contentContainerStyle={styles.listContainer}
        showsVerticalScrollIndicator={false}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#f8f9fa",
  },
  header: {
    flexDirection: "row",
    alignItems: "center",
    backgroundColor: "#fff",
    padding: 16,
    paddingTop: 50,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  backButton: {
    marginRight: 16,
  },
  backText: {
    fontSize: 24,
    color: "#007AFF",
    fontWeight: "bold",
  },
  headerTitle: {
    fontSize: 20,
    fontWeight: "bold",
    color: "#1a1a1a",
  },
  searchContainer: {
    backgroundColor: "#fff",
    padding: 16,
  },
  searchInput: {
    backgroundColor: "#f8f9fa",
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    borderWidth: 1,
    borderColor: "#e1e5e9",
  },
  listContainer: {
    padding: 16,
  },
  conversationItem: {
    flexDirection: "row",
    alignItems: "center",
    backgroundColor: "#fff",
    borderRadius: 12,
    padding: 16,
    marginBottom: 8,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.05,
    shadowRadius: 2,
    elevation: 2,
  },
  avatarContainer: {
    width: 50,
    height: 50,
    borderRadius: 25,
    backgroundColor: "#007AFF",
    justifyContent: "center",
    alignItems: "center",
    marginRight: 16,
  },
  avatarText: {
    color: "#fff",
    fontSize: 18,
    fontWeight: "bold",
  },
  conversationContent: {
    flex: 1,
  },
  conversationHeader: {
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    marginBottom: 4,
  },
  conversationName: {
    fontSize: 16,
    fontWeight: "600",
    color: "#1a1a1a",
  },
  timestamp: {
    fontSize: 12,
    color: "#666",
  },
  messageRow: {
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
  },
  lastMessage: {
    fontSize: 14,
    color: "#666",
    flex: 1,
    marginRight: 8,
  },
  unreadBadge: {
    backgroundColor: "#FF3B30",
    borderRadius: 10,
    minWidth: 20,
    height: 20,
    justifyContent: "center",
    alignItems: "center",
    paddingHorizontal: 6,
  },
  unreadText: {
    color: "#fff",
    fontSize: 12,
    fontWeight: "bold",
  },
});
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  1. Avatar generation: Create initials from name automatically
  2. Relative timestamps: Show time for recent messages, date for older ones
  3. useMemo for search: Optimize filtering performance
  4. Ternary for conditional rendering: Avoid rendering 0 as text
  5. numberOfLines prop: Truncate long messages automatically
  6. Alert for placeholder: Show native alert when chat not yet implemented

Takeaway: Chat UIs in mobile require more attention to spacing, touch targets, and performance. Avatar generation from initials is a common pattern when profile images aren't available.


Step 5: Building the Previous Tickets Screen

React (Web) - Ticket List:

// TicketList.js
function TicketList() {
  const getStatusColor = (status) => {
    switch (status) {
      case "open":
        return "green";
      case "closed":
        return "gray";
      case "pending":
        return "orange";
      default:
        return "gray";
    }
  };

  return (
    <div className="ticket-list">
      <div className="header">
        <button onClick={() => history.back()}>← Back</button>
        <h2>Previous Tickets</h2>
      </div>

      <div className="tickets">
        {tickets.map((ticket) => (
          <div key={ticket.id} className="ticket-item">
            <div className="ticket-header">
              <h3>{ticket.title}</h3>
              <span
                className="status-badge"
                style={{ backgroundColor: getStatusColor(ticket.status) }}
              >
                {ticket.status}
              </span>
            </div>
            <div className="ticket-meta">
              <span>{ticket.category}</span>
              <span className="priority">{ticket.priority}</span>
            </div>
            <div className="ticket-dates">
              <span>Created: {formatDate(ticket.createdAt)}</span>
              <span>Updated: {formatDate(ticket.updatedAt)}</span>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

React Native - FlatList with Status Badges:

// src/components/screens/PreviousTicketsScreen.tsx
import React from "react";
import {
  View,
  Text,
  StyleSheet,
  FlatList,
  TouchableOpacity,
  Alert,
} from "react-native";
import { useNavigation } from "@react-navigation/native";
import { mockSupportTickets } from "../../data/mockSupport";
import { SupportTicket } from "../../types/support";
import { SupportStackNavigationProp } from "../../types/navigation";

export default function PreviousTicketsScreen() {
  const navigation = useNavigation<SupportStackNavigationProp>();

  const handleTicketPress = (ticket: SupportTicket) => {
    Alert.alert(
      ticket.title,
      `Status: ${
        ticket.status.charAt(0).toUpperCase() + ticket.status.slice(1)
      }\nCategory: ${ticket.category}\nPriority: ${ticket.priority}`,
      [{ text: "OK" }]
    );
  };

  const getStatusColor = (status: string) => {
    switch (status) {
      case "open":
        return "#34C759";
      case "closed":
        return "#8E8E93";
      case "pending":
        return "#FF9500";
      default:
        return "#8E8E93";
    }
  };

  const getPriorityColor = (priority: string) => {
    switch (priority) {
      case "high":
        return "#FF3B30";
      case "medium":
        return "#FF9500";
      case "low":
        return "#34C759";
      default:
        return "#8E8E93";
    }
  };

  const formatDate = (dateString: string) => {
    const date = new Date(dateString);
    return date.toLocaleDateString("en-US", {
      month: "short",
      day: "numeric",
      year: "numeric",
    });
  };

  const renderTicket = ({ item }: { item: SupportTicket }) => (
    <TouchableOpacity
      style={styles.ticketItem}
      onPress={() => handleTicketPress(item)}
    >
      <View style={styles.ticketHeader}>
        <Text style={styles.ticketTitle}>{item.title}</Text>
        <View
          style={[
            styles.statusBadge,
            { backgroundColor: getStatusColor(item.status) },
          ]}
        >
          <Text style={styles.statusText}>{item.status}</Text>
        </View>
      </View>

      <View style={styles.ticketMeta}>
        <Text style={styles.ticketCategory}>{item.category}</Text>
        <View
          style={[
            styles.priorityBadge,
            { backgroundColor: getPriorityColor(item.priority) },
          ]}
        >
          <Text style={styles.priorityText}>{item.priority}</Text>
        </View>
      </View>

      <View style={styles.ticketFooter}>
        <Text style={styles.ticketDate}>
          Created: {formatDate(item.createdAt)}
        </Text>
        <Text style={styles.ticketDate}>
          Updated: {formatDate(item.updatedAt)}
        </Text>
      </View>
    </TouchableOpacity>
  );

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <TouchableOpacity
          style={styles.backButton}
          onPress={() => navigation.goBack()}
        >
          <Text style={styles.backText}></Text>
        </TouchableOpacity>
        <Text style={styles.headerTitle}>Previous Tickets</Text>
      </View>

      <FlatList
        data={mockSupportTickets}
        keyExtractor={(item) => item.id}
        renderItem={renderTicket}
        contentContainerStyle={styles.listContainer}
        showsVerticalScrollIndicator={false}
        ListEmptyComponent={
          <View style={styles.emptyContainer}>
            <Text style={styles.emptyText}>No previous tickets found</Text>
          </View>
        }
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#f8f9fa",
  },
  header: {
    flexDirection: "row",
    alignItems: "center",
    backgroundColor: "#fff",
    padding: 16,
    paddingTop: 50,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  backButton: {
    marginRight: 16,
  },
  backText: {
    fontSize: 24,
    color: "#007AFF",
    fontWeight: "bold",
  },
  headerTitle: {
    fontSize: 20,
    fontWeight: "bold",
    color: "#1a1a1a",
  },
  listContainer: {
    padding: 16,
  },
  ticketItem: {
    backgroundColor: "#fff",
    borderRadius: 12,
    padding: 16,
    marginBottom: 12,
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  ticketHeader: {
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "flex-start",
    marginBottom: 8,
  },
  ticketTitle: {
    fontSize: 16,
    fontWeight: "600",
    color: "#1a1a1a",
    flex: 1,
    marginRight: 12,
  },
  statusBadge: {
    paddingHorizontal: 8,
    paddingVertical: 4,
    borderRadius: 12,
  },
  statusText: {
    color: "#fff",
    fontSize: 12,
    fontWeight: "600",
    textTransform: "capitalize",
  },
  ticketMeta: {
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    marginBottom: 12,
  },
  ticketCategory: {
    fontSize: 14,
    color: "#666",
  },
  priorityBadge: {
    paddingHorizontal: 8,
    paddingVertical: 4,
    borderRadius: 12,
  },
  priorityText: {
    color: "#fff",
    fontSize: 12,
    fontWeight: "600",
    textTransform: "capitalize",
  },
  ticketFooter: {
    flexDirection: "row",
    justifyContent: "space-between",
  },
  ticketDate: {
    fontSize: 12,
    color: "#666",
  },
  emptyContainer: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    paddingTop: 100,
  },
  emptyText: {
    fontSize: 16,
    color: "#666",
    textAlign: "center",
  },
});
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  1. Dynamic badge colors: Status and priority determine background colors
  2. textTransform style: Capitalize badge text automatically
  3. ListEmptyComponent: Show friendly message when no tickets exist
  4. Multiple badges: Status and priority both displayed with different colors
  5. Date formatting: Convert ISO strings to readable dates
  6. Alert for details: Show quick summary in native alert

Takeaway: Badge systems in mobile need careful color choices for accessibility. Using iOS Human Interface Guidelines colors (#34C759, #FF9500, #FF3B30) ensures consistency with system UI.


Step 6: Nested Navigation with SupportStackNavigator

React (Web) - React Router:

// App.js
import { BrowserRouter, Routes, Route } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<MainLayout />}>
          <Route path="support" element={<SupportTab />} />
          <Route path="support/chat" element={<ChatScreen />} />
          <Route path="support/tickets" element={<TicketList />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}
Enter fullscreen mode Exit fullscreen mode

React Native - Stack Navigator:

// src/navigation/SupportStackNavigator.tsx
import React from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { SupportStackParamList } from "../types/navigation";

import SupportTab from "../components/screens/SupportTab";
import ChatScreen from "../components/screens/ChatScreen";
import PreviousTicketsScreen from "../components/screens/PreviousTicketsScreen";
import VideoConsultationScreen from "../components/screens/VideoConsultationScreen";

const Stack = createNativeStackNavigator<SupportStackParamList>();

export default function SupportStackNavigator() {
  return (
    <Stack.Navigator
      screenOptions={{
        headerShown: false,
      }}
    >
      <Stack.Screen name="Support" component={SupportTab} />
      <Stack.Screen name="Chat" component={ChatScreen} />
      <Stack.Screen name="PreviousTickets" component={PreviousTicketsScreen} />
      <Stack.Screen
        name="VideoConsultation"
        component={VideoConsultationScreen}
      />
    </Stack.Navigator>
  );
}
Enter fullscreen mode Exit fullscreen mode
// src/types/navigation.ts
export type SupportStackParamList = {
  Support: undefined;
  Chat: undefined;
  PreviousTickets: undefined;
  VideoConsultation: {
    center: import("./appointment").Center;
    consultant: import("./appointment").Consultant;
    sessionType: import("./appointment").SessionType;
  };
};

export type SupportStackNavigationProp =
  import("@react-navigation/native-stack").NativeStackNavigationProp<SupportStackParamList>;
Enter fullscreen mode Exit fullscreen mode

Key Differences:

  1. Nested stacks: Support tab gets its own stack navigator
  2. Type-safe navigation: TypeScript enforces correct screen names and params
  3. Shared screens: VideoConsultation reused from appointment flow
  4. Header control: Custom headers in each screen for consistency

Takeaway: React Navigation's stack navigator gives you native transitions (slide from right on iOS, slide from bottom on Android) automatically. Nested navigators let you create isolated flows within tabs.


Key Learnings

1. Action-First Mobile UX

Mobile support UIs prioritize quick access to common actions. Instead of comprehensive dashboards, show 2-3 large action cards that users can tap immediately.

2. Avatar Generation Pattern

When profile images aren't available, generate avatars from initials:

const getInitials = (name: string) => {
  return name
    .split(" ")
    .map((n) => n[0])
    .join("")
    .slice(0, 2);
};
Enter fullscreen mode Exit fullscreen mode

This creates a more personal experience than generic placeholder icons.

3. Relative Time Formatting

Chat and messaging UIs benefit from smart time displays:

const formatTimestamp = (timestamp: string) => {
  const date = new Date(timestamp);
  const now = new Date();
  const diffInHours = Math.floor(
    (now.getTime() - date.getTime()) / (1000 * 60 * 60)
  );

  if (diffInHours < 24) {
    return date.toLocaleTimeString("en-US", {
      hour: "numeric",
      minute: "2-digit",
      hour12: true,
    });
  } else {
    return date.toLocaleDateString("en-US", {
      month: "short",
      day: "numeric",
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

Show "2:30 PM" for today's messages, "Dec 7" for older ones.

4. Badge Color Systems

Consistent color coding helps users scan status quickly:

const STATUS_COLORS = {
  open: "#34C759", // Green
  closed: "#8E8E93", // Gray
  pending: "#FF9500", // Orange
};

const PRIORITY_COLORS = {
  high: "#FF3B30", // Red
  medium: "#FF9500", // Orange
  low: "#34C759", // Green
};
Enter fullscreen mode Exit fullscreen mode

Use iOS system colors for familiarity and accessibility.

5. Avoiding Rendering Falsy Values

React Native throws errors when rendering numbers like 0:

// ❌ Bad - renders 0 as text when unreadCount is 0
{item.unreadCount && <Badge count={item.unreadCount} />}

// ✅ Good - explicitly checks for > 0
{item.unreadCount && item.unreadCount > 0 && <Badge count={item.unreadCount} />}

// ✅ Better - uses ternary
{item.unreadCount && item.unreadCount > 0 ? <Badge count={item.unreadCount} /> : null}
Enter fullscreen mode Exit fullscreen mode

Always use ternary operators for conditional rendering in React Native.

6. ListEmptyComponent for Better UX

FlatList has built-in empty state support:

<FlatList
  data={items}
  renderItem={renderItem}
  ListEmptyComponent={
    <View style={styles.emptyContainer}>
      <Text style={styles.emptyText}>No tickets found</Text>
    </View>
  }
/>
Enter fullscreen mode Exit fullscreen mode

This shows friendly messages instead of blank screens.

7. Reusing Screens Across Navigators

The VideoConsultation screen is reused in both HomeStack and SupportStack:

// Both navigators can use the same screen
<Stack.Screen name="VideoConsultation" component={VideoConsultationScreen} />
Enter fullscreen mode Exit fullscreen mode

This reduces code duplication and ensures consistency.


Common Patterns & Best Practices

1. Action Card Design Pattern

const ActionCard = ({ icon, title, description, color, onPress }) => (
  <TouchableOpacity
    style={[styles.card, { borderLeftColor: color }]}
    onPress={onPress}
  >
    <View style={styles.iconContainer}>
      <Text style={styles.icon}>{icon}</Text>
    </View>
    <View style={styles.content}>
      <Text style={styles.title}>{title}</Text>
      <Text style={styles.description}>{description}</Text>
    </View>
  </TouchableOpacity>
);
Enter fullscreen mode Exit fullscreen mode

When to use: Landing screens, dashboards, quick action pages

2. Search Input Pattern

<View style={styles.searchContainer}>
  <TextInput
    style={styles.searchInput}
    placeholder="Search..."
    value={searchQuery}
    onChangeText={setSearchQuery}
    autoCapitalize="none"
    autoCorrect={false}
  />
</View>
Enter fullscreen mode Exit fullscreen mode

Key props:

  • autoCapitalize="none": Don't capitalize first letter for search
  • autoCorrect={false}: Don't autocorrect names/terms

3. Avatar Circle Pattern

const AvatarCircle = ({ name, size = 50 }) => {
  const initials = name
    .split(" ")
    .map((n) => n[0])
    .join("")
    .slice(0, 2);

  return (
    <View style={[styles.avatar, { width: size, height: size }]}>
      <Text style={styles.avatarText}>{initials}</Text>
    </View>
  );
};
Enter fullscreen mode Exit fullscreen mode

Styling tip: Use borderRadius: size / 2 for perfect circles

4. Status Badge Pattern

const StatusBadge = ({ status, colorMap }) => (
  <View style={[styles.badge, { backgroundColor: colorMap[status] }]}>
    <Text style={styles.badgeText}>{status}</Text>
  </View>
);

const styles = StyleSheet.create({
  badge: {
    paddingHorizontal: 8,
    paddingVertical: 4,
    borderRadius: 12,
  },
  badgeText: {
    color: "#fff",
    fontSize: 12,
    fontWeight: "600",
    textTransform: "capitalize",
  },
});
Enter fullscreen mode Exit fullscreen mode

When to use: Status indicators, priority levels, category tags

5. Back Button Pattern

<TouchableOpacity
  style={styles.backButton}
  onPress={() => navigation.goBack()}
>
  <Text style={styles.backText}></Text>
</TouchableOpacity>
Enter fullscreen mode Exit fullscreen mode

Alternative: Use navigation.pop() if you need more control over stack


Common Questions (If You're Coming from React)

Q: Should I use FlatList or map() for chat/ticket lists?

A: FlatList for lists that might grow (chat conversations from API). map() for small, fixed lists (3-5 action cards). FlatList virtualizes; map() renders everything.

Q: How do I handle real-time chat updates?

A: Use WebSockets (socket.io) or Firebase Realtime Database. Update state when new messages arrive. FlatList re-renders automatically. Will cover in Phase 8 when connecting backend.

Q: Can I use chat libraries instead of building custom?

A: Yes! react-native-gifted-chat and stream-chat-react-native are excellent. I built custom to learn patterns, but libraries save time in production.

Q: How do I generate random avatar colors?

A: Hash the user's name/ID to consistently generate the same color:

const hashCode = (str) => {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = str.charCodeAt(i) + ((hash << 5) - hash);
  }
  return hash;
};

const getAvatarColor = (name) => {
  const colors = ["#007AFF", "#34C759", "#FF9500", "#FF3B30", "#5856D6"];
  return colors[Math.abs(hashCode(name)) % colors.length];
};
Enter fullscreen mode Exit fullscreen mode

Q: How do I implement push notifications for chat?

A: Use Expo Notifications API or Firebase Cloud Messaging (FCM). Will cover in Phase 8 when adding real backend integration.

Q: Should I use Alert or Modal for confirmations?

A: Alert for simple confirmations (1-2 buttons). Modal for complex interactions (forms, multiple options). Alert is native, Modal is React Native.

Q: How do I handle keyboard with chat input?

A: Use KeyboardAvoidingView wrapper. For chat, you'll also need KeyboardSpacer or react-native-keyboard-aware-scroll-view. Will cover when building actual chat interface in Phase 8.

Q: Can I use Formik or React Hook Form for search?

A: Overkill for simple search. Plain useState is perfect. Save form libraries for complex multi-field forms with validation.

Q: How do I implement pull-to-refresh for chat/tickets?

A: FlatList has built-in refreshControl prop:

<FlatList
  data={items}
  renderItem={renderItem}
  refreshControl={
    <RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} />
  }
/>
Enter fullscreen mode Exit fullscreen mode

Resources That Helped Me


Code Repository

All the code from Phase 6 is available on GitHub:


Final Thoughts

Phase 6 was about building support and communication features—the essential help system every healthcare app needs. Building a support hub taught me how mobile UX differs from web: action cards over menus, conversation lists before messages, and status badges for quick scanning.

The biggest learning was understanding mobile-first UX patterns. On web, we'd build a support page with a comprehensive dashboard showing everything. On mobile, we show 3 large action cards and let users navigate to details. This "action-first" approach is faster and more intuitive on small screens.

What surprised me most was how much thought goes into seemingly simple things like avatar generation, timestamp formatting, and badge colors. These micro-interactions define the user experience. A well-formatted timestamp ("2:30 PM" vs "14:30:00") and a clearly color-coded status badge make the app feel polished and professional.

The conditional rendering pattern for unread counts taught me a valuable lesson: React Native is stricter than React web about falsy values. You can't let 0 slip through—it will render as text. Always use ternary operators for conditional UI in React Native.

Next up: Building the Profile section (Phase 7) with patient details, clinical records, payments, and regimen lists. This will introduce more complex data structures and nested information displays.

If you're a React developer, the transition to building support and chat features will feel familiar. The filtering logic is identical. The challenges are UX patterns (action cards, avatars, badges) and performance (useMemo for search, FlatList for lists). But once you learn these patterns, you'll find mobile UX more focused and user-friendly than web.

Top comments (0)