diff --git a/components/BalanceCard.tsx b/components/BalanceCard.tsx new file mode 100644 index 0000000..50fce03 --- /dev/null +++ b/components/BalanceCard.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { COLORS, FONTS } from '../constants/theme'; +import { BalanceInfo } from '../types'; +import { formatRupiah } from '../utils/helpers'; + +interface BalanceCardProps { + balance: BalanceInfo; +} + +export const BalanceCard: React.FC = ({ balance }) => { + return ( + + + + Pemasukan + + +{formatRupiah(balance.income)} + + + + Pengeluaran + + -{formatRupiah(balance.expense)} + + + + + + Total Saldo + + {formatRupiah( +balance.total +)} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: COLORS.card, + margin: 16, + padding: 20, + borderRadius: 12, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 3, + }, + row: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + col: { + flex: 1, + alignItems: 'center', + }, + label: { + fontSize: FONTS.small, + color: COLORS.textSecondary, + marginBottom: 4, + }, + amount: { + fontSize: FONTS.regular, + fontWeight: '600', + }, + income: { + color: COLORS.income, + }, + expense: { + color: COLORS.expense, + }, + divider: { + height: 1, + backgroundColor: COLORS.border, + marginVertical: 16, + }, + totalContainer: { + alignItems: 'center', + }, + totalLabel: { + fontSize: FONTS.small, + color: COLORS.textSecondary, + marginBottom: 4, + }, + totalAmount: { + fontSize: FONTS.xlarge, + fontWeight: 'bold', + color: COLORS.text, + }, +}); \ No newline at end of file diff --git a/components/header.tsx b/components/header.tsx new file mode 100644 index 0000000..9a89874 --- /dev/null +++ b/components/header.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +import { COLORS, FONTS } from '../constants/theme'; + +interface HeaderProps { + title: string; +} + +export const Header: React.FC = ({ title }) => { + return ( + + {title} + + ); +}; + +const styles = StyleSheet.create({ + container: { + backgroundColor: COLORS.primary, + padding: 20, + paddingTop: 50, + alignItems: 'center', + }, + title: { + fontSize: FONTS.large, + fontWeight: 'bold', + color: COLORS.card, + }, +}); \ No newline at end of file diff --git a/constants/theme.ts b/constants/theme.ts index f06facd..2f4b9e1 100644 --- a/constants/theme.ts +++ b/constants/theme.ts @@ -1,53 +1,19 @@ -/** - * Below are the colors that are used in the app. The colors are defined in the light and dark mode. - * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. - */ +export const COLORS = { + income: '#4CAF50', + expense: '#F44336', + background: '#F5F5F5', + card: '#FFFFFF', + text: '#333333', + textSecondary: '#666666', + border: '#E0E0E0', + primary: '#2196F3', +} as const; -import { Platform } from 'react-native'; +export const FONTS = { + regular: 16, + small: 14, + large: 24, + xlarge: 32, +} as const; -const tintColorLight = '#0a7ea4'; -const tintColorDark = '#fff'; - -export const Colors = { - light: { - text: '#11181C', - background: '#fff', - tint: tintColorLight, - icon: '#687076', - tabIconDefault: '#687076', - tabIconSelected: tintColorLight, - }, - dark: { - text: '#ECEDEE', - background: '#151718', - tint: tintColorDark, - icon: '#9BA1A6', - tabIconDefault: '#9BA1A6', - tabIconSelected: tintColorDark, - }, -}; - -export const Fonts = Platform.select({ - ios: { - /** iOS `UIFontDescriptorSystemDesignDefault` */ - sans: 'system-ui', - /** iOS `UIFontDescriptorSystemDesignSerif` */ - serif: 'ui-serif', - /** iOS `UIFontDescriptorSystemDesignRounded` */ - rounded: 'ui-rounded', - /** iOS `UIFontDescriptorSystemDesignMonospaced` */ - mono: 'ui-monospace', - }, - default: { - sans: 'normal', - serif: 'serif', - rounded: 'normal', - mono: 'monospace', - }, - web: { - sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif", - serif: "Georgia, 'Times New Roman', serif", - rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif", - mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", - }, -}); +14 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7aec5b0..8aa1a65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@expo/vector-icons": "^15.0.3", + "@react-native-async-storage/async-storage": "2.2.0", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", @@ -2797,6 +2798,18 @@ } } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", + "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", @@ -7855,6 +7868,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -8833,6 +8855,18 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", diff --git a/package.json b/package.json index f329e65..17567d8 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@expo/vector-icons": "^15.0.3", + "@react-native-async-storage/async-storage": "2.2.0", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", @@ -31,11 +32,11 @@ "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", - "react-native-worklets": "0.5.1", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", - "react-native-web": "~0.21.0" + "react-native-web": "~0.21.0", + "react-native-worklets": "0.5.1" }, "devDependencies": { "@types/react": "~19.1.0", diff --git a/types/index.ts b/types/index.ts new file mode 100644 index 0000000..c3a99ce --- /dev/null +++ b/types/index.ts @@ -0,0 +1,16 @@ +export interface Transaction { + id: string; + amount: number; + description: string; + type: 'income' | 'expense'; + category: string; + date: string; + +} + +export interface BalanceInfo { + total: number; + income: number; + expense: number; +} + diff --git a/utils/helpers.ts b/utils/helpers.ts new file mode 100644 index 0000000..93d4d3f --- /dev/null +++ b/utils/helpers.ts @@ -0,0 +1,45 @@ +import { Transaction, BalanceInfo } from '../types'; + +export const formatRupiah = (amount: number): string => { + return new Intl.NumberFormat('id-ID', { + style: 'currency', + currency: 'IDR', + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(amount); +}; + +export const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return new Intl.DateTimeFormat('id-ID', { + day: 'numeric', + month: 'short', + year: 'numeric', + }).format(date); +}; + +export const calculateBalance = (transactions: Transaction[]): BalanceInfo => { + const income = transactions + .filter((t) => t.type === 'income') + .reduce((sum, t) => sum + t.amount, 0); + + const expense = transactions + .filter((t) => t.type === 'expense') + .reduce((sum, t) => sum + t.amount, 0); + + return { + total: income - expense, + income, + expense, + }; +}; + +export const generateId = (): any => { + return +Date.now +().toString(36) + Math.random().toString(36).substr(2); +}; + +export const getCurrentDate = (): string => { + return new Date().toISOString(); +}; \ No newline at end of file