Skip to content

Commit 31b86d4

Browse files
authored
Merge pull request #80 from Resgrid/develop
Develop
2 parents 7215464 + 0579c0d commit 31b86d4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+3344
-302
lines changed

env.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ const client = z.object({
8888
LOGGING_KEY: z.string(),
8989
APP_KEY: z.string(),
9090
MAPBOX_PUBKEY: z.string(),
91-
MAPBOX_DLKEY: z.string(),
9291
IS_MOBILE_APP: z.boolean(),
9392
SENTRY_DSN: z.string(),
9493
COUNTLY_APP_KEY: z.string(),
@@ -125,7 +124,6 @@ const _clientEnv = {
125124
APP_KEY: process.env.DISPATCH_APP_KEY || '',
126125
IS_MOBILE_APP: true, // or whatever default you want
127126
MAPBOX_PUBKEY: process.env.DISPATCH_MAPBOX_PUBKEY || '',
128-
MAPBOX_DLKEY: process.env.DISPATCH_MAPBOX_DLKEY || '',
129127
SENTRY_DSN: process.env.DISPATCH_SENTRY_DSN || '',
130128
COUNTLY_APP_KEY: process.env.DISPATCH_COUNTLY_APP_KEY || '',
131129
COUNTLY_SERVER_URL: process.env.DISPATCH_COUNTLY_SERVER_URL || '',

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"prebuild": "cross-env EXPO_NO_DOTENV=1 yarn expo prebuild",
1010
"android": "cross-env EXPO_NO_DOTENV=1 expo run:android",
1111
"ios": "cross-env EXPO_NO_DOTENV=1 expo run:ios --device",
12-
"web": "cross-env EXPO_NO_DOTENV=1 expo start --web",
12+
"web": "cross-env expo start --web --clear",
1313
"xcode": "xed -b ios",
1414
"doctor": "npx expo-doctor@latest",
1515
"start:staging": "cross-env APP_ENV=staging yarn run start",

src/app/(app)/_layout.tsx

Lines changed: 155 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Redirect, Slot } from 'expo-router';
77
import { Menu } from 'lucide-react-native';
88
import React, { useCallback, useEffect, useRef } from 'react';
99
import { useTranslation } from 'react-i18next';
10-
import { ActivityIndicator, Platform, StyleSheet } from 'react-native';
10+
import { ActivityIndicator, Platform, StyleSheet, Text as RNText, TouchableOpacity, View as RNView } from 'react-native';
1111
import { useSafeAreaInsets } from 'react-native-safe-area-context';
1212

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

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

63+
// Web dark mode detection (safe - only runs on web)
64+
React.useEffect(() => {
65+
if (Platform.OS !== 'web') return;
66+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
67+
setWebColorScheme(mediaQuery.matches ? 'dark' : 'light');
68+
const handler = (e: MediaQueryListEvent) => setWebColorScheme(e.matches ? 'dark' : 'light');
69+
mediaQuery.addEventListener('change', handler);
70+
return () => mediaQuery.removeEventListener('change', handler);
71+
}, []);
72+
6273
// Refs to track initialization state
6374
const hasInitialized = useRef(false);
6475
const isInitializing = useRef(false);
@@ -348,75 +359,66 @@ export default function TabLayout() {
348359
},
349360
});
350361

351-
const content = (
352-
<View style={styles.container}>
353-
{/* Top Navigation Bar */}
354-
<View className="flex-row items-center justify-between bg-primary-600 px-4" style={{ paddingTop: insets.top }}>
355-
<CreateDrawerMenuButton setIsOpen={setIsOpen} />
356-
<View className="flex-1 items-center">
357-
<Text className="text-lg font-semibold text-white">{t('app.title', 'Resgrid Responder')}</Text>
362+
// Web theme with dark mode support (matching panel header and button colors)
363+
const webIsDark = webColorScheme === 'dark';
364+
const webTheme = {
365+
navBar: { backgroundColor: webIsDark ? '#1f2937' : '#f9fafb' }, // gray-800 / gray-50 (panel header colors)
366+
navBarText: { color: webIsDark ? '#f9fafb' : '#030712' }, // gray-50 / gray-950
367+
sidebar: {
368+
backgroundColor: webIsDark ? '#030712' : '#f3f4f6', // gray-950 / gray-100
369+
borderRightColor: webIsDark ? '#1f2937' : '#e5e7eb', // gray-800 / gray-200
370+
},
371+
sidebarFooter: {
372+
borderTopColor: webIsDark ? '#1f2937' : '#e5e7eb',
373+
backgroundColor: webIsDark ? '#111827' : '#ffffff', // gray-900 / white
374+
},
375+
closeButton: { backgroundColor: '#2563eb' }, // blue-600 (panel button color)
376+
closeButtonText: { color: '#ffffff' },
377+
mainContent: { backgroundColor: webIsDark ? '#030712' : '#f3f4f6' }, // gray-950 / gray-100
378+
};
379+
380+
const content =
381+
Platform.OS === 'web' ? (
382+
<RNView style={styles.container}>
383+
{/* Top Navigation Bar */}
384+
<RNView style={[layoutStyles.navBar, { paddingTop: insets.top }, webTheme.navBar]}>
385+
<CreateDrawerMenuButton setIsOpen={setIsOpen} colorScheme={webColorScheme} />
386+
<RNView style={layoutStyles.navBarTitle}>
387+
<RNText style={[layoutStyles.navBarTitleText, webTheme.navBarText]}>{t('app.title', 'Resgrid Responder')}</RNText>
388+
</RNView>
389+
</RNView>
390+
391+
<RNView style={{ flex: 1, flexDirection: 'row' }} ref={parentRef}>
392+
{/* Sidebar - simple show/hide */}
393+
{isOpen ? (
394+
<RNView style={[layoutStyles.webSidebar, webTheme.sidebar]}>
395+
<SideMenu onNavigate={handleNavigate} colorScheme={webColorScheme} />
396+
<RNView style={[layoutStyles.sidebarFooter, webTheme.sidebarFooter]}>
397+
<TouchableOpacity onPress={() => setIsOpen(false)} style={[layoutStyles.closeButton, webTheme.closeButton]}>
398+
<RNText style={[layoutStyles.closeButtonText, webTheme.closeButtonText]}>{t('menu.close', 'Close Menu')}</RNText>
399+
</TouchableOpacity>
400+
</RNView>
401+
</RNView>
402+
) : null}
403+
404+
{/* Main content area */}
405+
<RNView style={[layoutStyles.mainContent, webTheme.mainContent]}>
406+
<Slot />
407+
</RNView>
408+
</RNView>
409+
</RNView>
410+
) : (
411+
<View style={styles.container}>
412+
{/* Top Navigation Bar */}
413+
<View className="flex-row items-center justify-between bg-primary-600 px-4" style={{ paddingTop: insets.top }}>
414+
<CreateDrawerMenuButton setIsOpen={setIsOpen} />
415+
<View className="flex-1 items-center">
416+
<Text className="text-lg font-semibold text-white">{t('app.title', 'Resgrid Responder')}</Text>
417+
</View>
358418
</View>
359-
</View>
360419

361-
<View className="flex-1" ref={parentRef}>
362-
{/* Drawer menu - always rendered as modal, closed by default */}
363-
{Platform.OS === 'web' ? (
364-
// Web-specific drawer implementation with fixed positioning
365-
isOpen && (
366-
<View
367-
// @ts-ignore - web specific styles
368-
style={{
369-
position: 'fixed',
370-
top: 0,
371-
left: 0,
372-
right: 0,
373-
bottom: 0,
374-
zIndex: 9999,
375-
display: 'flex',
376-
flexDirection: 'row',
377-
}}
378-
>
379-
{/* Backdrop */}
380-
<Pressable
381-
onPress={() => setIsOpen(false)}
382-
// @ts-ignore - web specific styles
383-
style={{
384-
position: 'absolute',
385-
top: 0,
386-
left: 0,
387-
right: 0,
388-
bottom: 0,
389-
backgroundColor: 'rgba(0, 0, 0, 0.5)',
390-
}}
391-
/>
392-
{/* Drawer Content */}
393-
<View
394-
className="bg-white dark:bg-gray-900"
395-
// @ts-ignore - web specific styles
396-
style={{
397-
position: 'relative',
398-
width: '80%',
399-
maxWidth: 320,
400-
height: '100%',
401-
display: 'flex',
402-
flexDirection: 'column',
403-
zIndex: 1,
404-
boxShadow: '2px 0 8px rgba(0, 0, 0, 0.15)',
405-
}}
406-
>
407-
<View style={{ flex: 1, overflow: 'scroll' as 'visible' | 'hidden' | 'scroll' }}>
408-
<SideMenu onNavigate={handleNavigate} />
409-
</View>
410-
<View className="border-t border-gray-200 p-4 dark:border-gray-700">
411-
<Button onPress={() => setIsOpen(false)} className="w-full bg-primary-600">
412-
<ButtonText>Close</ButtonText>
413-
</Button>
414-
</View>
415-
</View>
416-
</View>
417-
)
418-
) : (
419-
// Native drawer implementation
420+
<View className="flex-1" ref={parentRef}>
421+
{/* Native drawer implementation */}
420422
<Drawer isOpen={isOpen} onClose={() => setIsOpen(false)}>
421423
<DrawerBackdrop onPress={() => setIsOpen(false)} />
422424
<DrawerContent className="w-4/5 max-w-xs bg-white p-0 dark:bg-gray-900">
@@ -430,15 +432,14 @@ export default function TabLayout() {
430432
</DrawerFooter>
431433
</DrawerContent>
432434
</Drawer>
433-
)}
434435

435-
{/* Main content area */}
436-
<View className="w-full flex-1">
437-
<Slot />
436+
{/* Main content area */}
437+
<View className="w-full flex-1">
438+
<Slot />
439+
</View>
438440
</View>
439441
</View>
440-
</View>
441-
);
442+
);
442443

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

466467
interface CreateDrawerMenuButtonProps {
467468
setIsOpen: (isOpen: boolean) => void;
469+
colorScheme?: 'light' | 'dark';
468470
}
469471

470-
const CreateDrawerMenuButton = ({ setIsOpen }: CreateDrawerMenuButtonProps) => {
472+
const CreateDrawerMenuButton = ({ setIsOpen, colorScheme }: CreateDrawerMenuButtonProps) => {
473+
// Use React Native primitives on web to avoid infinite render loops from gluestack-ui/lucide
474+
if (Platform.OS === 'web') {
475+
const isDark = colorScheme === 'dark';
476+
return (
477+
<TouchableOpacity onPress={() => setIsOpen(true)} testID="drawer-menu-button" style={layoutStyles.menuButton}>
478+
<RNText style={[layoutStyles.menuIcon, { color: isDark ? '#f9fafb' : '#030712' }]}></RNText>
479+
</TouchableOpacity>
480+
);
481+
}
482+
471483
return (
472484
<Pressable
473485
className="p-2"
@@ -506,3 +518,71 @@ const styles = StyleSheet.create({
506518
height: '100%',
507519
},
508520
});
521+
522+
const layoutStyles = StyleSheet.create({
523+
navBar: {
524+
flexDirection: 'row',
525+
alignItems: 'center',
526+
justifyContent: 'space-between',
527+
paddingHorizontal: 16,
528+
paddingVertical: 12,
529+
shadowColor: '#000',
530+
shadowOffset: { width: 0, height: 2 },
531+
shadowOpacity: 0.1,
532+
shadowRadius: 3,
533+
elevation: 3,
534+
},
535+
navBarTitle: {
536+
flex: 1,
537+
alignItems: 'center',
538+
},
539+
navBarTitleText: {
540+
fontSize: 18,
541+
fontWeight: '700',
542+
color: 'white',
543+
letterSpacing: 0.5,
544+
},
545+
menuButton: {
546+
padding: 8,
547+
borderRadius: 6,
548+
},
549+
menuIcon: {
550+
fontSize: 24,
551+
color: 'white',
552+
fontWeight: 'bold',
553+
},
554+
webSidebar: {
555+
width: 280,
556+
borderRightWidth: 1,
557+
shadowColor: '#000',
558+
shadowOffset: { width: 2, height: 0 },
559+
shadowOpacity: 0.1,
560+
shadowRadius: 3,
561+
elevation: 2,
562+
},
563+
sidebarFooter: {
564+
borderTopWidth: 1,
565+
padding: 16,
566+
},
567+
mainContent: {
568+
flex: 1,
569+
},
570+
backdrop: {
571+
position: 'absolute',
572+
top: 0,
573+
left: 0,
574+
right: 0,
575+
bottom: 0,
576+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
577+
},
578+
closeButton: {
579+
padding: 12,
580+
borderRadius: 8,
581+
alignItems: 'center',
582+
},
583+
closeButtonText: {
584+
color: 'white',
585+
fontWeight: '600',
586+
fontSize: 14,
587+
},
588+
});

0 commit comments

Comments
 (0)