Skip to content

Commit b065289

Browse files
authored
feat: Dashboard layout design & sidebar (#41)
1 parent 160a326 commit b065289

File tree

9 files changed

+373
-6
lines changed

9 files changed

+373
-6
lines changed

__tests__/components/sidebar.test.tsx

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from 'react';
2+
import { render, fireEvent, waitFor } from '@testing-library/react-native';
3+
import { useRouter, usePathname } from 'expo-router';
4+
import Sidebar from '@/components/sidebar'; // Adjust the import path as needed
5+
6+
// Mock the expo-router hooks
7+
jest.mock('expo-router', () => ({
8+
useRouter: jest.fn(),
9+
usePathname: jest.fn(),
10+
}));
11+
12+
// Mock the SvgXml component
13+
jest.mock('react-native-svg', () => ({
14+
SvgXml: 'SvgXml',
15+
}));
16+
17+
describe('Sidebar', () => {
18+
const mockOnClose = jest.fn();
19+
const mockPush = jest.fn();
20+
21+
beforeEach(() => {
22+
jest.clearAllMocks();
23+
(useRouter as jest.Mock).mockReturnValue({ push: mockPush });
24+
(usePathname as jest.Mock).mockReturnValue('/dashboard');
25+
});
26+
27+
it('renders correctly', () => {
28+
const { getByText } = render(<Sidebar onClose={mockOnClose} />);
29+
expect(getByText('Dashboard')).toBeTruthy();
30+
expect(getByText('Attendance')).toBeTruthy();
31+
expect(getByText('Performance')).toBeTruthy();
32+
expect(getByText('Calendar')).toBeTruthy();
33+
expect(getByText('Docs')).toBeTruthy();
34+
expect(getByText('Help')).toBeTruthy();
35+
expect(getByText('LogOut')).toBeTruthy();
36+
});
37+
38+
it('highlights the active item based on current pathname', () => {
39+
(usePathname as jest.Mock).mockReturnValue('/dashboard/trainee');
40+
const { getByText } = render(<Sidebar onClose={mockOnClose} />);
41+
const attendanceItem = getByText('Attendance').parent;
42+
expect(attendanceItem?.props.style).toEqual(
43+
expect.objectContaining({
44+
backgroundColor: expect.stringContaining('indigo')
45+
})
46+
);
47+
});
48+
49+
it('calls onClose when close button is pressed', () => {
50+
const { getByTestId } = render(<Sidebar onClose={mockOnClose} />);
51+
const closeButton = getByTestId('close-button');
52+
fireEvent.press(closeButton);
53+
expect(mockOnClose).toHaveBeenCalled();
54+
});
55+
56+
it('navigates and closes sidebar when an item is pressed', async () => {
57+
const { getByText } = render(<Sidebar onClose={mockOnClose} />);
58+
fireEvent.press(getByText('Attendance'));
59+
60+
await waitFor(() => {
61+
expect(mockPush).toHaveBeenCalledWith('/dashboard/trainee');
62+
expect(mockOnClose).toHaveBeenCalled();
63+
});
64+
});
65+
66+
});

app/_layout.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ function RootLayoutNav() {
6060
<Stack>
6161
<Stack.Screen name="(onboarding)" options={{ headerShown: false }} />
6262
<Stack.Screen name="auth" options={{ headerShown: false }} />
63+
<Stack.Screen name="dashboard" options={{ headerShown: false }}/>
6364
</Stack>
6465
</ThemeProvider>
6566
</ApolloProvider>

app/dashboard/_layout.tsx

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { lightLogoIcon, darkLogoIcon, menu, lightNotifyIcon, darkNotifyIcon } from '@/assets/Icons/dashboard/Icons';
2+
import { Slot } from 'expo-router';
3+
import { useEffect, useState } from 'react';
4+
import { KeyboardAvoidingView, Platform, ScrollView, TouchableOpacity, View, useColorScheme, Image } from 'react-native';
5+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
6+
import { SvgXml } from 'react-native-svg';
7+
import Sidebar from '@/components/sidebar';
8+
9+
export default function AuthLayout() {
10+
const insets = useSafeAreaInsets();
11+
const [isSidebarOpen, setIsSidebarOpen] = useState<boolean>(false);
12+
const colorScheme = useColorScheme();
13+
14+
const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen);
15+
16+
17+
return (
18+
<KeyboardAvoidingView
19+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
20+
style={{ flex: 1 }}
21+
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
22+
className="bg-primary-light dark:bg-primary-dark"
23+
>
24+
<ScrollView
25+
contentContainerStyle={{ flexGrow: 1 }}
26+
keyboardShouldPersistTaps="handled"
27+
bounces={false}
28+
style={{
29+
paddingTop: insets.top,
30+
paddingBottom: insets.bottom,
31+
paddingLeft: insets.left,
32+
paddingRight: insets.right,
33+
}}
34+
>
35+
<View className={`bg-primary-light dark:bg-primary-dark h-full px-5`}>
36+
<View className="w-full h-[60px] relative bg-primary-light dark:bg-primary-dark flex justify-center ">
37+
<View className="flex-row justify-between">
38+
<View className="flex-row">
39+
<TouchableOpacity onPress={toggleSidebar}>
40+
<SvgXml xml={menu} width={40} height={40} />
41+
</TouchableOpacity>
42+
<SvgXml
43+
xml={colorScheme === 'dark' ? darkLogoIcon : lightLogoIcon}
44+
width={100}
45+
height={40}
46+
/>
47+
</View>
48+
<View className="flex-row gap-5">
49+
<TouchableOpacity>
50+
<SvgXml
51+
xml={colorScheme === 'dark' ? darkNotifyIcon : lightNotifyIcon}
52+
width={25}
53+
height={40}
54+
/>
55+
</TouchableOpacity>
56+
<TouchableOpacity>
57+
<Image
58+
source={require('@/assets/images/profilePic.png')}
59+
style={{ width: 40, height: 40 }}
60+
/>
61+
</TouchableOpacity>
62+
</View>
63+
</View>
64+
</View>
65+
<Slot />
66+
</View>
67+
</ScrollView>
68+
{isSidebarOpen && (
69+
<View className="absolute top-0 left-0 bottom-0">
70+
<Sidebar
71+
onClose={toggleSidebar}
72+
/>
73+
</View>
74+
)}
75+
</KeyboardAvoidingView>
76+
);
77+
}

app/dashboard/index.tsx

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
import { Text, View } from 'react-native';
2-
3-
export default function Dashboard() {
1+
import { StyleSheet, Text, useColorScheme, View } from 'react-native';
2+
import { PropsWithChildren } from 'react';
3+
import React from 'react';
4+
export const CustomText = ({ children }: PropsWithChildren) => <Text>{children}</Text>;
5+
const Dashboard = () => {
6+
const colorScheme = useColorScheme();
47
return (
58
<View>
6-
<Text>Dashboard.</Text>
9+
<Text className={`ml-2 text-base ${colorScheme === 'light' ? 'text-black' : 'text-white'}`}>Dashboard Coming soon</Text>
710
</View>
811
);
9-
}
12+
};
13+
14+
export default Dashboard;
15+
16+
const styles = StyleSheet.create({});

app/dashboard/trainee/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Text, View } from 'react-native';
33
export default function TraineeDashboard() {
44
return (
55
<View>
6-
<Text>Trainee Dashboard.</Text>
6+
<Text>Trainee Dashboard Coming Soon</Text>
77
</View>
88
);
99
}

assets/Icons/auth/Icons.tsx

+1
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)