Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
44 changes: 44 additions & 0 deletions react-native/DashboardChallenge/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local
.env
.env.*
!.env.example

# typescript
*.tsbuildinfo

# env
.env
.env.*
!.env.example
55 changes: 55 additions & 0 deletions react-native/DashboardChallenge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Dashboard challenge app


## Features

### 📱 User Interface
- View and track all financial transactions
- Detailed transaction modal view
- Offline data persistence
- Pull-to-refresh functionality
- Error handling with graceful fallbacks
- Loading states with skeleton screens

## Technical Stack

- **Framework**: React Native with Expo
- **Navigation**: Expo Router
- **State Management**: React Context API
- **Storage**: AsyncStorage for offline persistence
- **Styling**: React Native StyleSheet
- **Animations**: React Native Animated API


## State Management

The app uses React Context for global state management, handling:
- Transaction data
- Product data
- Loading states
- Error states
- Balance calculations

## Offline Support

- Transactions and products are cached locally
- Automatic fallback to cached data when offline
- Background sync when connection is restored
- Error handling for failed API requests

## Getting Started

1. Clone the repository

2. Add a .env file containing :
```bash
EXPO_PUBLIC_API_URL=https://628b46b07886bbbb37b46173.mockapi.io/api/v1
```
3. Install dependencies:
```bash
npm install
```
4. Start the development server:
```bash
npx expo start
```
34 changes: 34 additions & 0 deletions react-native/DashboardChallenge/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"expo": {
"name": "DashboardChallenge",
"slug": "DashboardChallenge",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"favicon": "./assets/favicon.png",
"bundler": "metro"
},
"scheme": "dashboardchallenge",
"plugins": [
"expo-router",
"expo-font"
]
}
}
29 changes: 29 additions & 0 deletions react-native/DashboardChallenge/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Stack } from 'expo-router';
import { AppProvider } from './context/AppContext';

export default function RootLayout() {
return (
<AppProvider>
<Stack
screenOptions={{
headerStyle: {
backgroundColor: '#fff',
},
headerShadowVisible: false,
}}>
<Stack.Screen
name="screens/dashboard/index"
options={{
title: 'Dashboard'
}}
/>
<Stack.Screen
name="screens/transactions/index"
options={{
title: 'Transactions'
}}
/>
</Stack>
</AppProvider>
);
}
20 changes: 20 additions & 0 deletions react-native/DashboardChallenge/app/components/Balance/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import { View, Text } from 'react-native';
import { useBalance } from '../../hooks/useBalance';
import { styles } from './styles';
import BalanceSkeleton from '../BalanceSkeleton';

export default function Balance() {
const { balance, isLoading } = useBalance();

if (isLoading) {
return <BalanceSkeleton />;
}

return (
<View style={styles.balanceContainer}>
<Text style={styles.label}>Balance</Text>
<Text style={styles.balance}>€{balance.toFixed(2)}</Text>
</View>
);
}
19 changes: 19 additions & 0 deletions react-native/DashboardChallenge/app/components/Balance/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { StyleSheet } from 'react-native';

export const styles = StyleSheet.create({
balanceContainer: {
padding: 20,
backgroundColor: '#fff',
marginVertical: 10,
},
label: {
fontSize: 16,
color: '#666',
},
balance: {
fontSize: 24,
fontWeight: 'bold',
},
});

export default {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import { View } from 'react-native';
import Skeleton from '../Skeleton';
import { styles } from './styles';

export default function BalanceSkeleton() {
return (
<View style={styles.container}>
<Skeleton width={60} height={16} style={styles.label} />
<Skeleton width={120} height={24} style={styles.amount} />
</View>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { StyleSheet } from 'react-native';

export const styles = StyleSheet.create({
container: {
padding: 20,
backgroundColor: '#fff',
marginVertical: 10,
},
label: {
marginBottom: 8,
},
amount: {
marginTop: 4,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { View, Text } from 'react-native';
import { styles } from './styles';

interface Props {
message: string;
}

export default function EmptyState({ message }: Props) {
return (
<View style={styles.container}>
<Text style={styles.message}>{message}</Text>
</View>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { StyleSheet } from 'react-native';

export const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
message: {
fontSize: 16,
color: '#666',
textAlign: 'center',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { View, Dimensions } from 'react-native';
import Skeleton from '../Skeleton';
import { styles } from './styles';

const { width } = Dimensions.get('window');

export default function ProductSkeleton() {
return (
<View style={styles.container}>
<Skeleton
width={width - 40}
height={200}
style={styles.skeleton}
/>
</View>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { StyleSheet } from 'react-native';

export const styles = StyleSheet.create({
container: {
marginHorizontal: 20,
},
skeleton: {
borderRadius: 10,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useEffect } from 'react';
import { View, Image, FlatList, Dimensions } from 'react-native';
import { styles } from './styles';
import { useProducts } from '../../hooks/useProducts';
import ProductSkeleton from '../ProductSkeleton';
import EmptyState from '../EmptyState';

const { width } = Dimensions.get('window');

export default function ProductsCarousel() {
const { products, refetch, isLoading, error } = useProducts();

useEffect(() => {
refetch();
}, [refetch]);

if (isLoading) {
return (
<View style={styles.container}>
<ProductSkeleton />
</View>
);
}

return (
<View style={styles.container}>
<FlatList
data={products}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
renderItem={({ item }) => (
<Image
source={{ uri: item.image }}
style={[styles.image, { width: width - 40 }]}
resizeMode="cover"
/>
)}
keyExtractor={item => item.id}
refreshing={isLoading}
onRefresh={refetch}
ListEmptyComponent={
<View style={[styles.emptyContainer,{marginLeft: 40}]}>
<EmptyState
message={
error
? "Failed to load products. Pull to refresh."
: "No products available."
}
/>
</View>
}
/>
</View>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { StyleSheet } from 'react-native';

export const styles = StyleSheet.create({
container: {
marginVertical: 10,
},
image: {
height: 200,
marginHorizontal: 20,
borderRadius: 10,
},
emptyContainer: {
height: 200,
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 20,
}
});

export default {};
Loading