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:
- Support Tab with personalized greeting and three action cards
- Action cards for Chat, Video Consultation, and Previous Tickets
- Chat screen with conversation list and avatar generation
- Search functionality for filtering conversations
- Previous Tickets screen with status badges and priority indicators
- Nested navigation using SupportStackNavigator
- Timestamp formatting for chat messages (relative time)
- 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
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
Key Differences:
- Dedicated stack navigator: Support features get their own navigation stack
- Action-first design: Main support screen focuses on quick actions
- Chat as list view: Chat UI starts with conversation list, not message threads
- 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",
},
];
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";
Benefits of These Types:
- Status tracking: Union types ensure valid status values
-
Optional fields:
unreadCountonly when there are unread messages - Priority system: Three-level priority for ticket management
- Action types: Type-safe action routing in support screen
- 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>
);
}
/* 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;
}
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,
},
});
Key Differences:
- ScrollView instead of grid: Better for mobile scrolling
- TouchableOpacity for cards: Native press feedback
- Navigation over links: Stack navigation for smooth transitions
- Elevation/shadow combo: Android elevation + iOS shadow
- Personalized greeting: Using first name from auth context
- 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>
);
}
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",
},
});
Key Differences:
- Avatar generation: Create initials from name automatically
- Relative timestamps: Show time for recent messages, date for older ones
- useMemo for search: Optimize filtering performance
-
Ternary for conditional rendering: Avoid rendering
0as text - numberOfLines prop: Truncate long messages automatically
- 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>
);
}
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",
},
});
Key Differences:
- Dynamic badge colors: Status and priority determine background colors
- textTransform style: Capitalize badge text automatically
- ListEmptyComponent: Show friendly message when no tickets exist
- Multiple badges: Status and priority both displayed with different colors
- Date formatting: Convert ISO strings to readable dates
- 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>
);
}
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>
);
}
// 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>;
Key Differences:
- Nested stacks: Support tab gets its own stack navigator
- Type-safe navigation: TypeScript enforces correct screen names and params
- Shared screens: VideoConsultation reused from appointment flow
- 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);
};
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",
});
}
};
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
};
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}
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>
}
/>
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} />
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>
);
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>
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>
);
};
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",
},
});
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>
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];
};
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} />
}
/>
Resources That Helped Me
- React Navigation - Nesting Navigators - Nested stack patterns
- React Native - FlatList - List optimization
- React Native - Alert - Native alerts API
- useMemo Hook - Search optimization
- iOS Human Interface Guidelines - Colors - System colors
Code Repository
All the code from Phase 6 is available on GitHub:
- physio-care-react-native-first-project - Complete source code
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)