Skip to content
Open
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
5 changes: 5 additions & 0 deletions react-native/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// https://docs.expo.dev/guides/using-eslint/
module.exports = {
extends: 'expo',
ignorePatterns: ['/dist/*'],
};
38 changes: 38 additions & 0 deletions react-native/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 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

# typescript
*.tsbuildinfo

app-example
98 changes: 61 additions & 37 deletions react-native/README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,61 @@
# React Native App Dashboard Challenge

## Overview
Create a React Native application that gives users a comprehensive dashboard by displaying their dynamically calculated account balance and a list of recent transactions.

## Requirements

### User Stories
- **Account Balance:** Users can view their calculated account balance.
- **Recent Transactions:** The dashboard shows the five most recent transactions.
- **Full Transaction History:** Users can tap the "See all" button to view their complete transaction list.

### API Endpoints
Fetch data from the following endpoints:
- **Products:** [https://628b46b07886bbbb37b46173.mockapi.io/api/v1/products](https://628b46b07886bbbb37b46173.mockapi.io/api/v1/products)
- **Transactions:** [https://628b46b07886bbbb37b46173.mockapi.io/api/v1/transactions](https://628b46b07886bbbb37b46173.mockapi.io/api/v1/transactions)

### Wireframes
Refer to the provided wireframes for guidance on layout and design:

| Dashboard | Transactions |
| --------- | :----------: |
| ![Dashboard](dashboard.png) | ![Transactions](transactions.png) |

## Nice-to-Have Features
- **Pull to Refresh:** Allow users to update the dashboard by pulling down on the list.
- **Offline Support:** Cache data locally for offline viewing.
- **Skeleton Loading Screens:** Display placeholders or spinners while data is loading.
- **Modal View for Transactions:** When a user taps on a transaction, display a modal with detailed information about that transaction.

## Challenge Instructions
- **Fork the repository.**
- **Build a clean, well-structured, and performant solution.**
- **Commit your work frequently.**
- **Submit your solution via a pull request.**

Happy coding!
# Account Dashboard App

A React Native application that provides users with a comprehensive financial dashboard showing their account balance and transaction history.

## Features

### Core Features

- **Account Balance**: Real-time calculated account balance display
- **Recent Transactions**: View three most recent transactions on the dashboard
- **Full Transaction History**: Access complete transaction list via "See all" button

### Additional Features

- **Pull to Refresh**: Update dashboard data by pulling down on the transaction list
- **Dark/Light Mode**: Toggle between light and dark themes for better user experience
- **Offline Support**: Cache data locally for viewing when offline
- **Connection Status**: Display components to notify users of offline status and when connection restores using NetInfo
- **Skeleton Loading Screens**: Show placeholder loaders while data is being fetched
- **Modal Transaction Details**: Tap on any transaction to view detailed information in a modal

## Technologies Used

- React Native Expo
- Expo Router
- React Query (TanStack Query) for data fetching, caching, and state management
- AsyncStorage for persistent offline data storage
- NetInfo for network connectivity detection
- NativeWind

## Project Structure

```
project-root/
├── app/
│ ├── _layout.tsx
│ ├── +not-found.tsx
│ ├── index.tsx
│ └── transactions.tsx
├── assets/
├── components/
│ ├── shared/
│ │ ├── Amount.tsx
│ │ ├── ConnectionStatus.tsx
│ │ ├── EmptyData.tsx
│ │ ├── ScreenWrapper.tsx
│ │ ├── Skeleton.tsx
│ │ ├── Summary.tsx
│ │ └── TransactionItem.tsx
│ ├── ProductsCarousel.tsx
│ ├── RecentTransactions.tsx
│ └── ShowFullTransactions.tsx
├── config/
│ └── api-config.ts
├── utils/
│ └── index.ts
└── hooks/
├── useColorScheme.ts
├── useProducts.ts
└── useTransactions.ts
```
43 changes: 43 additions & 0 deletions react-native/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"expo": {
"name": "rand-challenge",
"slug": "rand-challenge",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "myapp",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.somias.rand-challenge",
"appleTeamId": "9V2Q5VWT8B"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
[
"expo-splash-screen",
{
"image": "./assets/images/splash-icon.png",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#ffffff"
}
]
],
"experiments": {
"typedRoutes": true
}
}
}
29 changes: 29 additions & 0 deletions react-native/app/+not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Link, Stack } from "expo-router";
import { StyleSheet, View, Text } from "react-native";

export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: "Oops!" }} />
<View style={styles.container}>
<Text>This screen doesn't exist.</Text>
<Link href="/" style={styles.link}>
<Text>Go to home screen!</Text>
</Link>
</View>
</>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 20,
},
link: {
marginTop: 15,
paddingVertical: 15,
},
});
79 changes: 79 additions & 0 deletions react-native/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
DarkTheme,
DefaultTheme,
ThemeProvider,
} from "@react-navigation/native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { QueryClient } from "@tanstack/react-query";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { StatusBar } from "expo-status-bar";
import { useEffect } from "react";
import "react-native-reanimated";

import "../global.css";

import { useColorScheme } from "@/hooks/useColorScheme";

// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();

const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
});

const queryClient = new QueryClient({
defaultOptions: {
queries: {
networkMode: "offlineFirst",
gcTime: 1000 * 60 * 60 * 24, // 24 hours
},
},
});

export default function RootLayout() {
const colorScheme = useColorScheme();
const [loaded] = useFonts({
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
});

useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
}
}, [loaded]);

if (!loaded) {
return null;
}

return (
<PersistQueryClientProvider
persistOptions={{ persister: asyncStoragePersister }}
client={queryClient}
>
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen
name="index"
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="transactions"
options={{
presentation: "modal",
headerTitle: "Transactions",
}}
/>
<Stack.Screen name="+not-found" />
</Stack>
<StatusBar style="auto" />
</ThemeProvider>
</PersistQueryClientProvider>
);
}
44 changes: 44 additions & 0 deletions react-native/app/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useCallback } from "react";
import ScreenWrapper from "@/components/shared/ScreenWrapper";
import { RefreshControl, ScrollView, View } from "react-native";
import ProductsCarousel from "@/components/ProductsCarousel";
import Summary from "@/components/shared/Summary";
import RecentTransaction from "@/components/RecentTransactions";

import { useTransactions } from "@/hooks/useTransactions";
import { useProducts } from "@/hooks/useProducts";

export default function HomeRoute() {
const {
refetch: refetchTransactions,
isRefetching: isRefetchingTransactions,
} = useTransactions();
const { refetch: refetchProducts, isRefetching: isRefetchingProducts } =
useProducts();

const isRefreshing = isRefetchingTransactions || isRefetchingProducts;

const handleRefresh = useCallback(async () => {
await Promise.all([refetchTransactions(), refetchProducts()]);
}, [refetchTransactions, refetchProducts]);

return (
<ScreenWrapper>
<View className="p-4">
<ProductsCarousel />
</View>
<ScrollView
className="flex-1"
refreshControl={
<RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} />
}
contentContainerClassName="p-4 flex-1 gap-6 dark:bg-gray-900 text-white bg-gray-100 text-black"
>
<View className="gap-6 flex-1 mt-10">
<Summary />
<RecentTransaction />
</View>
</ScrollView>
</ScreenWrapper>
);
}
18 changes: 18 additions & 0 deletions react-native/app/transactions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react";
import ScreenWrapper from "@/components/shared/ScreenWrapper";
import { View } from "react-native";
import Summary from "@/components/shared/Summary";
import RecentTransactions from "@/components/RecentTransactions";

export default function TransactionsScreen() {
return (
<ScreenWrapper>
<View className="flex-1 p-4">
<View className="gap-6 flex-1">
<Summary expandSummary />
<RecentTransactions showFull />
</View>
</View>
</ScreenWrapper>
);
}
Binary file added react-native/assets/fonts/SpaceMono-Regular.ttf
Binary file not shown.
Binary file added react-native/assets/images/adaptive-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added react-native/assets/images/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added react-native/assets/images/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added react-native/assets/images/partial-react-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added react-native/assets/images/react-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added react-native/assets/images/react-logo@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added react-native/assets/images/react-logo@3x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added react-native/assets/images/splash-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions react-native/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = function (api) {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
};
};
Loading