Skip to content

Commit cac29f9

Browse files
committed
feat: add dark theme support for navigation and paper components
1 parent 6fe980e commit cac29f9

File tree

5 files changed

+98
-32
lines changed

5 files changed

+98
-32
lines changed

frontend/App.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1+
import { useColorScheme } from 'react-native';
12
import { GestureHandlerRootView } from 'react-native-gesture-handler';
23
import { PaperProvider } from 'react-native-paper';
34
import { AuthProvider } from './context/AuthContext';
45
import AppNavigator from './navigation/AppNavigator';
5-
import { paperTheme } from './utils/theme';
6+
import { paperTheme, paperThemeDark } from './utils/theme';
67

78
export default function App() {
9+
const scheme = useColorScheme();
10+
const theme = scheme === 'dark' ? paperThemeDark : paperTheme;
811
return (
912
<AuthProvider>
10-
<PaperProvider theme={paperTheme}>
13+
<PaperProvider theme={theme}>
1114
<GestureHandlerRootView style={{ flex: 1 }}>
1215
<AppNavigator />
1316
</GestureHandlerRootView>

frontend/navigation/AppNavigator.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { NavigationContainer } from '@react-navigation/native';
22
import { useContext } from 'react';
3-
import { ActivityIndicator, StyleSheet, View } from 'react-native';
3+
import { ActivityIndicator, StyleSheet, useColorScheme, View } from 'react-native';
44
import { AuthContext } from '../context/AuthContext';
5-
import { navTheme } from '../utils/theme';
5+
import { navTheme, navThemeDark } from '../utils/theme';
66
import AuthNavigator from './AuthNavigator';
77
import MainNavigator from './MainNavigator';
88

@@ -17,8 +17,10 @@ const AppNavigator = () => {
1717
);
1818
}
1919

20+
const scheme = useColorScheme();
21+
const theme = scheme === 'dark' ? navThemeDark : navTheme;
2022
return (
21-
<NavigationContainer theme={navTheme}>
23+
<NavigationContainer theme={theme}>
2224
{token ? <MainNavigator /> : <AuthNavigator />}
2325
</NavigationContainer>
2426
);

frontend/screens/GroupDetailsScreen.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useTheme } from "@react-navigation/native";
2-
import { useContext, useEffect, useState } from "react";
2+
import { useContext, useEffect, useMemo, useState } from "react";
33
import { Alert, FlatList, StyleSheet, Text, View } from "react-native";
44
import {
55
ActivityIndicator,
@@ -68,10 +68,13 @@ const GroupDetailsScreen = ({ route, navigation }) => {
6868
}
6969
}, [token, groupId]);
7070

71-
const getMemberName = (userId) => {
72-
const member = members.find((m) => m.userId === userId);
73-
return member ? member.user.name : "Unknown";
74-
};
71+
const membersMap = useMemo(() => {
72+
const map = new Map();
73+
members.forEach((m) => map.set(m.userId, m.user?.name || 'Unknown'));
74+
return map;
75+
}, [members]);
76+
77+
const getMemberName = (userId) => membersMap.get(userId) || 'Unknown';
7578

7679
const renderExpense = ({ item }) => {
7780
const userSplit = item.splits.find((s) => s.userId === user._id);
@@ -230,8 +233,8 @@ const styles = StyleSheet.create({
230233
alignItems: "center",
231234
},
232235
card: {
233-
marginBottom: 16,
234-
borderRadius: 16,
236+
marginBottom: 16,
237+
borderRadius: 16,
235238
},
236239
expensesTitle: {
237240
marginTop: 16,

frontend/screens/HomeScreen.js

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useTheme } from "@react-navigation/native";
2-
import { useContext, useEffect, useRef, useState } from "react";
2+
import { useCallback, useContext, useEffect, useRef, useState } from "react";
33
import { Alert, Animated, FlatList, Pressable, StyleSheet, View } from "react-native";
44
import Swipeable from "react-native-gesture-handler/Swipeable";
55
import {
@@ -16,11 +16,11 @@ import {
1616
} from "react-native-paper";
1717
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
1818
import { AuthContext } from "../context/AuthContext";
19-
import { formatCurrency, getCurrencySymbol } from "../utils/currency";
19+
import { formatCurrency } from "../utils/currency";
2020
import { tokens } from "../utils/theme";
2121

2222
const HomeScreen = ({ navigation }) => {
23-
const { token, logout, user } = useContext(AuthContext);
23+
const { token, user } = useContext(AuthContext);
2424
const { colors } = useTheme();
2525
const [groups, setGroups] = useState([]);
2626
const [isLoading, setIsLoading] = useState(true);
@@ -80,17 +80,23 @@ const HomeScreen = ({ navigation }) => {
8080

8181
// Fetch settlement status for each group
8282
if (user?._id) {
83-
const settlementPromises = groupsList.map(async (group) => {
84-
const status = await calculateSettlementStatus(group._id, user._id);
85-
return { groupId: group._id, status };
86-
});
87-
88-
const settlementResults = await Promise.all(settlementPromises);
89-
const settlementMap = {};
90-
settlementResults.forEach(({ groupId, status }) => {
91-
settlementMap[groupId] = status;
92-
});
93-
setGroupSettlements(settlementMap);
83+
const chunkSize = 5;
84+
// clear previous while loading fresh data
85+
setGroupSettlements({});
86+
for (let i = 0; i < groupsList.length; i += chunkSize) {
87+
const slice = groupsList.slice(i, i + chunkSize);
88+
const results = await Promise.all(
89+
slice.map(async (group) => ({
90+
groupId: group._id,
91+
status: await calculateSettlementStatus(group._id, user._id),
92+
}))
93+
);
94+
const partial = results.reduce((acc, { groupId, status }) => {
95+
acc[groupId] = status;
96+
return acc;
97+
}, {});
98+
setGroupSettlements((prev) => ({ ...prev, ...partial }));
99+
}
94100
}
95101
} catch (error) {
96102
console.error("Failed to fetch groups:", error);
@@ -125,7 +131,7 @@ const HomeScreen = ({ navigation }) => {
125131
}
126132
};
127133

128-
const currencySymbol = getCurrencySymbol();
134+
// const currencySymbol = getCurrencySymbol(); // not used on this screen currently
129135

130136
// Simple mount animation for list items
131137
const itemAnim = useRef(new Animated.Value(0)).current;
@@ -137,7 +143,7 @@ const HomeScreen = ({ navigation }) => {
137143
}).start();
138144
}, [groups]);
139145

140-
const renderGroup = ({ item, index }) => {
146+
const renderGroup = useCallback(({ item, index }) => {
141147
const settlementStatus = groupSettlements[item._id];
142148

143149
// Generate settlement status text
@@ -248,7 +254,12 @@ const HomeScreen = ({ navigation }) => {
248254
opacity: itemAnim,
249255
}}
250256
>
251-
<Swipeable renderRightActions={renderRightActions} renderLeftActions={renderLeftActions}>
257+
<Swipeable
258+
renderRightActions={renderRightActions}
259+
renderLeftActions={renderLeftActions}
260+
friction={2}
261+
overshootFriction={8}
262+
>
252263
<Pressable onPress={openDetails} onPressIn={onPressIn} onPressOut={onPressOut}>
253264
<Card style={[styles.card, { backgroundColor: colors.card }]}>
254265
<Card.Title
@@ -277,7 +288,7 @@ const HomeScreen = ({ navigation }) => {
277288
</Swipeable>
278289
</Animated.View>
279290
);
280-
};
291+
}, [groupSettlements, colors, itemAnim, navigation]);
281292

282293
return (
283294
<View style={styles.container}>
@@ -326,6 +337,9 @@ const HomeScreen = ({ navigation }) => {
326337
renderItem={renderGroup}
327338
keyExtractor={(item) => item._id}
328339
contentContainerStyle={styles.list}
340+
initialNumToRender={8}
341+
windowSize={10}
342+
removeClippedSubviews
329343
ListEmptyComponent={
330344
<Text style={styles.emptyText}>
331345
No groups found. Create or join one!

frontend/utils/theme.js

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// Centralized theme for react-native-paper (MD3) and React Navigation
22
// Formal, modern palette suitable for finance/expense tracking, with bold accents
33

4-
import { DefaultTheme as DefaultNavTheme } from '@react-navigation/native';
5-
import { MD3LightTheme as DefaultPaperTheme } from 'react-native-paper';
4+
import { DarkTheme as DefaultNavDark, DefaultTheme as DefaultNavTheme } from '@react-navigation/native';
5+
import { MD3DarkTheme as DefaultPaperDark, MD3LightTheme as DefaultPaperTheme } from 'react-native-paper';
66

77
const palette = {
88
// Neutrals
@@ -58,3 +58,47 @@ export const navTheme = {
5858
};
5959

6060
export const tokens = palette;
61+
62+
// Dark theme variant
63+
const darkPalette = {
64+
...palette,
65+
background: '#0B1220',
66+
surface: '#0F172A',
67+
surfaceVariant: '#111827',
68+
outline: '#1F2937',
69+
primary: '#3B82F6',
70+
text: '#E5E7EB',
71+
textMuted: '#9CA3AF',
72+
};
73+
74+
export const paperThemeDark = {
75+
...DefaultPaperDark,
76+
colors: {
77+
...DefaultPaperDark.colors,
78+
primary: darkPalette.primary,
79+
onPrimary: darkPalette.onPrimary,
80+
secondary: darkPalette.secondary,
81+
tertiary: darkPalette.tertiary,
82+
background: darkPalette.background,
83+
surface: darkPalette.surface,
84+
surfaceVariant: darkPalette.surfaceVariant,
85+
outline: darkPalette.outline,
86+
error: darkPalette.danger,
87+
onSurface: darkPalette.text,
88+
onSurfaceVariant: darkPalette.textMuted,
89+
},
90+
roundness: 12,
91+
};
92+
93+
export const navThemeDark = {
94+
...DefaultNavDark,
95+
colors: {
96+
...DefaultNavDark.colors,
97+
primary: darkPalette.primary,
98+
background: darkPalette.background,
99+
card: darkPalette.surface,
100+
text: darkPalette.text,
101+
border: darkPalette.outline,
102+
notification: darkPalette.secondary,
103+
},
104+
};

0 commit comments

Comments
 (0)