Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions env.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ const client = z.object({
LOGGING_KEY: z.string(),
APP_KEY: z.string(),
MAPBOX_PUBKEY: z.string(),
MAPBOX_DLKEY: z.string(),
IS_MOBILE_APP: z.boolean(),
SENTRY_DSN: z.string(),
COUNTLY_APP_KEY: z.string(),
Expand Down Expand Up @@ -125,7 +124,6 @@ const _clientEnv = {
APP_KEY: process.env.DISPATCH_APP_KEY || '',
IS_MOBILE_APP: true, // or whatever default you want
MAPBOX_PUBKEY: process.env.DISPATCH_MAPBOX_PUBKEY || '',
MAPBOX_DLKEY: process.env.DISPATCH_MAPBOX_DLKEY || '',
SENTRY_DSN: process.env.DISPATCH_SENTRY_DSN || '',
COUNTLY_APP_KEY: process.env.DISPATCH_COUNTLY_APP_KEY || '',
COUNTLY_SERVER_URL: process.env.DISPATCH_COUNTLY_SERVER_URL || '',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"prebuild": "cross-env EXPO_NO_DOTENV=1 yarn expo prebuild",
"android": "cross-env EXPO_NO_DOTENV=1 expo run:android",
"ios": "cross-env EXPO_NO_DOTENV=1 expo run:ios --device",
"web": "cross-env EXPO_NO_DOTENV=1 expo start --web",
"web": "cross-env expo start --web --clear",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent EXPO_NO_DOTENV=1 usage across scripts.

The web script no longer sets EXPO_NO_DOTENV=1, while all other scripts (start, prebuild, android, ios, and build scripts on lines 8-11 and 28-35) consistently use it. This means .env files will be auto-loaded for web but not for other platforms, potentially causing different environment variable behavior between web and native development.

Was this intentional? If so, consider adding a comment explaining why web differs. Otherwise, restore consistency:

Suggested fix to restore consistency
-    "web": "cross-env expo start --web --clear",
+    "web": "cross-env EXPO_NO_DOTENV=1 expo start --web --clear",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"web": "cross-env expo start --web --clear",
"web": "cross-env EXPO_NO_DOTENV=1 expo start --web --clear",
🤖 Prompt for AI Agents
In `@package.json` at line 12, The "web" npm script currently omits the
EXPO_NO_DOTENV=1 prefix, causing inconsistent dotenv behavior versus other
scripts; update the "web" script (named "web") to include EXPO_NO_DOTENV=1 like
the other scripts, or if the difference is intentional, add an inline comment
near the "web" script explaining why it should load .env (or not) to make the
intent explicit and keep behavior consistent across "start", "prebuild",
"android", "ios", build scripts and "web".

"xcode": "xed -b ios",
"doctor": "npx expo-doctor@latest",
"start:staging": "cross-env APP_ENV=staging yarn run start",
Expand Down
230 changes: 155 additions & 75 deletions src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Redirect, Slot } from 'expo-router';
import { Menu } from 'lucide-react-native';
import React, { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { ActivityIndicator, Platform, StyleSheet } from 'react-native';
import { ActivityIndicator, Platform, StyleSheet, Text as RNText, TouchableOpacity, View as RNView } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { NotificationButton } from '@/components/notifications/NotificationButton';
Expand Down Expand Up @@ -41,6 +41,7 @@ export default function TabLayout() {
const [isFirstTime, _setIsFirstTime] = useIsFirstTime();
const [isOpen, setIsOpen] = React.useState(false);
const [isNotificationsOpen, setIsNotificationsOpen] = React.useState(false);
const [webColorScheme, setWebColorScheme] = React.useState<'light' | 'dark'>('light');

// Get store states first (hooks must be at top level)
const config = useCoreStore((state) => state.config);
Expand All @@ -59,6 +60,16 @@ export default function TabLayout() {
const { isActive, appState } = useAppLifecycle();
const insets = useSafeAreaInsets();

// Web dark mode detection (safe - only runs on web)
React.useEffect(() => {
if (Platform.OS !== 'web') return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
setWebColorScheme(mediaQuery.matches ? 'dark' : 'light');
const handler = (e: MediaQueryListEvent) => setWebColorScheme(e.matches ? 'dark' : 'light');
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, []);

// Refs to track initialization state
const hasInitialized = useRef(false);
const isInitializing = useRef(false);
Expand Down Expand Up @@ -348,75 +359,66 @@ export default function TabLayout() {
},
});

const content = (
<View style={styles.container}>
{/* Top Navigation Bar */}
<View className="flex-row items-center justify-between bg-primary-600 px-4" style={{ paddingTop: insets.top }}>
<CreateDrawerMenuButton setIsOpen={setIsOpen} />
<View className="flex-1 items-center">
<Text className="text-lg font-semibold text-white">{t('app.title', 'Resgrid Responder')}</Text>
// Web theme with dark mode support (matching panel header and button colors)
const webIsDark = webColorScheme === 'dark';
const webTheme = {
navBar: { backgroundColor: webIsDark ? '#1f2937' : '#f9fafb' }, // gray-800 / gray-50 (panel header colors)
navBarText: { color: webIsDark ? '#f9fafb' : '#030712' }, // gray-50 / gray-950
sidebar: {
backgroundColor: webIsDark ? '#030712' : '#f3f4f6', // gray-950 / gray-100
borderRightColor: webIsDark ? '#1f2937' : '#e5e7eb', // gray-800 / gray-200
},
sidebarFooter: {
borderTopColor: webIsDark ? '#1f2937' : '#e5e7eb',
backgroundColor: webIsDark ? '#111827' : '#ffffff', // gray-900 / white
},
closeButton: { backgroundColor: '#2563eb' }, // blue-600 (panel button color)
closeButtonText: { color: '#ffffff' },
mainContent: { backgroundColor: webIsDark ? '#030712' : '#f3f4f6' }, // gray-950 / gray-100
};

const content =
Platform.OS === 'web' ? (
<RNView style={styles.container}>
{/* Top Navigation Bar */}
<RNView style={[layoutStyles.navBar, { paddingTop: insets.top }, webTheme.navBar]}>
<CreateDrawerMenuButton setIsOpen={setIsOpen} colorScheme={webColorScheme} />
<RNView style={layoutStyles.navBarTitle}>
<RNText style={[layoutStyles.navBarTitleText, webTheme.navBarText]}>{t('app.title', 'Resgrid Responder')}</RNText>
</RNView>
</RNView>

<RNView style={{ flex: 1, flexDirection: 'row' }} ref={parentRef}>
{/* Sidebar - simple show/hide */}
{isOpen ? (
<RNView style={[layoutStyles.webSidebar, webTheme.sidebar]}>
<SideMenu onNavigate={handleNavigate} colorScheme={webColorScheme} />
<RNView style={[layoutStyles.sidebarFooter, webTheme.sidebarFooter]}>
<TouchableOpacity onPress={() => setIsOpen(false)} style={[layoutStyles.closeButton, webTheme.closeButton]}>
<RNText style={[layoutStyles.closeButtonText, webTheme.closeButtonText]}>{t('menu.close', 'Close Menu')}</RNText>
</TouchableOpacity>
</RNView>
</RNView>
) : null}

{/* Main content area */}
<RNView style={[layoutStyles.mainContent, webTheme.mainContent]}>
<Slot />
</RNView>
</RNView>
</RNView>
) : (
<View style={styles.container}>
{/* Top Navigation Bar */}
<View className="flex-row items-center justify-between bg-primary-600 px-4" style={{ paddingTop: insets.top }}>
<CreateDrawerMenuButton setIsOpen={setIsOpen} />
<View className="flex-1 items-center">
<Text className="text-lg font-semibold text-white">{t('app.title', 'Resgrid Responder')}</Text>
</View>
</View>
</View>

<View className="flex-1" ref={parentRef}>
{/* Drawer menu - always rendered as modal, closed by default */}
{Platform.OS === 'web' ? (
// Web-specific drawer implementation with fixed positioning
isOpen && (
<View
// @ts-ignore - web specific styles
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 9999,
display: 'flex',
flexDirection: 'row',
}}
>
{/* Backdrop */}
<Pressable
onPress={() => setIsOpen(false)}
// @ts-ignore - web specific styles
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
}}
/>
{/* Drawer Content */}
<View
className="bg-white dark:bg-gray-900"
// @ts-ignore - web specific styles
style={{
position: 'relative',
width: '80%',
maxWidth: 320,
height: '100%',
display: 'flex',
flexDirection: 'column',
zIndex: 1,
boxShadow: '2px 0 8px rgba(0, 0, 0, 0.15)',
}}
>
<View style={{ flex: 1, overflow: 'scroll' as 'visible' | 'hidden' | 'scroll' }}>
<SideMenu onNavigate={handleNavigate} />
</View>
<View className="border-t border-gray-200 p-4 dark:border-gray-700">
<Button onPress={() => setIsOpen(false)} className="w-full bg-primary-600">
<ButtonText>Close</ButtonText>
</Button>
</View>
</View>
</View>
)
) : (
// Native drawer implementation
<View className="flex-1" ref={parentRef}>
{/* Native drawer implementation */}
<Drawer isOpen={isOpen} onClose={() => setIsOpen(false)}>
<DrawerBackdrop onPress={() => setIsOpen(false)} />
<DrawerContent className="w-4/5 max-w-xs bg-white p-0 dark:bg-gray-900">
Expand All @@ -430,15 +432,14 @@ export default function TabLayout() {
</DrawerFooter>
</DrawerContent>
</Drawer>
)}

{/* Main content area */}
<View className="w-full flex-1">
<Slot />
{/* Main content area */}
<View className="w-full flex-1">
<Slot />
</View>
</View>
</View>
</View>
);
);

// On web, skip Novu integration as it may cause rendering issues
if (Platform.OS === 'web') {
Expand All @@ -465,9 +466,20 @@ export default function TabLayout() {

interface CreateDrawerMenuButtonProps {
setIsOpen: (isOpen: boolean) => void;
colorScheme?: 'light' | 'dark';
}

const CreateDrawerMenuButton = ({ setIsOpen }: CreateDrawerMenuButtonProps) => {
const CreateDrawerMenuButton = ({ setIsOpen, colorScheme }: CreateDrawerMenuButtonProps) => {
// Use React Native primitives on web to avoid infinite render loops from gluestack-ui/lucide
if (Platform.OS === 'web') {
const isDark = colorScheme === 'dark';
return (
<TouchableOpacity onPress={() => setIsOpen(true)} testID="drawer-menu-button" style={layoutStyles.menuButton}>
<RNText style={[layoutStyles.menuIcon, { color: isDark ? '#f9fafb' : '#030712' }]}>☰</RNText>
</TouchableOpacity>
);
}
Comment on lines +472 to +481
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add an accessibility label for the web menu button.

Right now the button is only an icon; screen readers won’t announce intent.

🛠️ Proposed fix
-    return (
-      <TouchableOpacity onPress={() => setIsOpen(true)} testID="drawer-menu-button" style={layoutStyles.menuButton}>
+    return (
+      <TouchableOpacity
+        onPress={() => setIsOpen(true)}
+        testID="drawer-menu-button"
+        accessibilityLabel="Open menu"
+        accessibilityRole="button"
+        style={layoutStyles.menuButton}
+      >
         <RNText style={[layoutStyles.menuIcon, { color: isDark ? '#f9fafb' : '#030712' }]}>☰</RNText>
       </TouchableOpacity>
     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const CreateDrawerMenuButton = ({ setIsOpen, colorScheme }: CreateDrawerMenuButtonProps) => {
// Use React Native primitives on web to avoid infinite render loops from gluestack-ui/lucide
if (Platform.OS === 'web') {
const isDark = colorScheme === 'dark';
return (
<TouchableOpacity onPress={() => setIsOpen(true)} testID="drawer-menu-button" style={layoutStyles.menuButton}>
<RNText style={[layoutStyles.menuIcon, { color: isDark ? '#f9fafb' : '#030712' }]}></RNText>
</TouchableOpacity>
);
}
const CreateDrawerMenuButton = ({ setIsOpen, colorScheme }: CreateDrawerMenuButtonProps) => {
// Use React Native primitives on web to avoid infinite render loops from gluestack-ui/lucide
if (Platform.OS === 'web') {
const isDark = colorScheme === 'dark';
return (
<TouchableOpacity
onPress={() => setIsOpen(true)}
testID="drawer-menu-button"
accessibilityLabel="Open menu"
accessibilityRole="button"
style={layoutStyles.menuButton}
>
<RNText style={[layoutStyles.menuIcon, { color: isDark ? '#f9fafb' : '#030712' }]}></RNText>
</TouchableOpacity>
);
}
🤖 Prompt for AI Agents
In `@src/app/`(app)/_layout.tsx around lines 472 - 481, The web menu button in
CreateDrawerMenuButton lacks an accessibility label; update the TouchableOpacity
returned when Platform.OS === 'web' to include an accessibilityLabel (e.g.,
"Open navigation menu") and set accessibilityRole="button" (and
accessible={true}) so screen readers announce its intent; locate the
TouchableOpacity in CreateDrawerMenuButton (the element with
testID="drawer-menu-button" and onPress={() => setIsOpen(true)}) and add these
props while preserving existing styles and behavior.


return (
<Pressable
className="p-2"
Expand Down Expand Up @@ -506,3 +518,71 @@ const styles = StyleSheet.create({
height: '100%',
},
});

const layoutStyles = StyleSheet.create({
navBar: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingVertical: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 3,
},
navBarTitle: {
flex: 1,
alignItems: 'center',
},
navBarTitleText: {
fontSize: 18,
fontWeight: '700',
color: 'white',
letterSpacing: 0.5,
},
menuButton: {
padding: 8,
borderRadius: 6,
},
menuIcon: {
fontSize: 24,
color: 'white',
fontWeight: 'bold',
},
webSidebar: {
width: 280,
borderRightWidth: 1,
shadowColor: '#000',
shadowOffset: { width: 2, height: 0 },
shadowOpacity: 0.1,
shadowRadius: 3,
elevation: 2,
},
sidebarFooter: {
borderTopWidth: 1,
padding: 16,
},
mainContent: {
flex: 1,
},
backdrop: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
closeButton: {
padding: 12,
borderRadius: 8,
alignItems: 'center',
},
closeButtonText: {
color: 'white',
fontWeight: '600',
fontSize: 14,
},
});
Loading