Skip to content
This repository was archived by the owner on May 5, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0fd5bc7
update: add initial README with project overview, features, tech stac…
bakaqc May 2, 2025
19d57ae
add: add initial test files for dateUtils and priceUtils
bakaqc May 2, 2025
7dea63c
add: create initial test files for hooks, rootReducer, and store
bakaqc May 2, 2025
81859a6
add: create initial test files for giftService, giftSlice, recipientS…
bakaqc May 2, 2025
b38095a
add: create initial test files for CalendarIntegration, ReminderSetti…
bakaqc May 2, 2025
c7b59da
add: update package.json to include test script and add new dependenc…
bakaqc May 2, 2025
675e36f
add: implement onIntegrate and onToggle props for CalendarIntegration…
bakaqc May 2, 2025
c4488ad
add: create tests for BottomTabBar and TabBarButton components to ver…
bakaqc May 2, 2025
b20b154
add: create tests for FilterTabs, GiftCard, and RecipientCard compone…
bakaqc May 2, 2025
5536cdc
add: implement tests for giftService and recipientService, including …
bakaqc May 2, 2025
2732e8f
add: implement tests for Redux hooks, rootReducer, and store to verif…
bakaqc May 2, 2025
32c1437
add: update package.json and bun.lock to include testing libraries; a…
bakaqc May 2, 2025
59f24a1
update: package.json and bun.lock to include testing libraries; add t…
bakaqc May 2, 2025
1548acd
add: update package.json and bun.lock to include react-test-renderer …
bakaqc May 3, 2025
1af57fe
Merge remote-tracking branch 'origin/feature/testing' into feature/te…
bakaqc May 3, 2025
9f34bd7
feat: add Jest configuration and setup for testing
bakaqc May 3, 2025
2924e33
Merge branch 'main' into feature/testing
bakaqc May 3, 2025
1709bdf
fix: correct import statement for Supabase environment variables
bakaqc May 3, 2025
1b80f76
Merge remote-tracking branch 'origin/feature/testing' into feature/te…
bakaqc May 3, 2025
bedf85a
update: package lock version
bakaqc May 4, 2025
eb02902
feat: enhance Supabase mock implementation for gift and recipient ser…
bakaqc May 4, 2025
84ed6eb
feat: update deleteGift return type and enhance Supabase mock for tes…
bakaqc May 4, 2025
5591e3f
feat: enhance README with AI-powered development workflow and tools used
bakaqc May 4, 2025
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
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 🎁 Gift Idea Tracker

A mobile application built with **React Native + Redux + Supabase**, designed to help users organize and track gift ideas for their friends, family, and special occasions — all in one place.

> 📆 Plan ahead. 🎁 Stay thoughtful. 💡 Never forget a gift again.

---

## 💡 AI-Powered Development Workflow

We leveraged **cutting-edge AI tools** throughout the entire software development lifecycle to accelerate productivity, improve quality, and stay creative:

| Phase | Tools Used |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 📋 Requirement Gathering | [ChatGPT](https://chat.openai.com) – Assist in defining user stories and features |
| 🎨 UI/UX Design | [Uizard](https://uizard.io) + Autodesigner 1.5 for AI wireframes and flows<br>[Figma](https://figma.com) + Codia AI plugin for auto-generating UI components |
| 💻 Development | [a0.dev](https://a0.dev) to generate boilerplate code from designs<br>[VSCode](https://code.visualstudio.com) with [GitHub Copilot](https://github.com/features/copilot) for live coding, bug fixes, and code suggestions |
| 🧪 Testing | Combination of **GitHub Copilot** and **ChatGPT** for writing test cases and debugging<br>Manual & automated testing via Jest + React Native Testing Library |

<br>

## ✨ Features

- 🧠 AI-assisted wireframes for fast UI prototyping
- 📋 Manage gift ideas with title, image, notes, and tags
- 👥 Add & manage recipients and event dates
- 💰 Track budgets for each recipient and overall spending
- 📊 Visual charts for budget analysis
- ⏰ Reminder & calendar sync for upcoming events
- 🔔 Push notifications (optional)
- ☁️ Data stored securely using Supabase


<br>

## 🛠️ Tech Stack

| Layer | Tools/Tech |
| ------------ | ----------------------------------------- |
| Frontend | React Native, Redux Toolkit, TypeScript |
| Backend | Supabase (PostgreSQL, Auth, Storage) |
| Design | Uizard, Figma (with Codia AI) |
| AI Assistant | ChatGPT, GitHub Copilot, a0.dev |
| Testing | Jest, React Native Testing Library, Detox |
| Build/Deploy | Expo, EAS Build, Google Play, TestFlight |
302 changes: 242 additions & 60 deletions bun.lock

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
preset: 'react-native',
transform: {
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(jest-)?react-native|@react-native|@react-navigation|react-native-reanimated|react-native-gesture-handler)',
],

moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1', // chỉnh lại nếu bạn dùng alias khác
},
};
14 changes: 14 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Jest setup file

// Mock global functions or modules if needed
jest.mock('react-native', () => {
const ReactNative = jest.requireActual('react-native');
return {
...ReactNative,
ActionSheetIOS: {
showActionSheetWithOptions: jest.fn(),
},
};
});

// Add any other global mocks or configurations here
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios"
"ios": "expo start --ios",
"test": "jest"
},
"dependencies": {
"@expo/config-plugins": "~9.0.0",
"@expo/vector-icons": "14.1.0",
"@react-native-community/datetimepicker": "8.2.0",
"@reduxjs/toolkit": "^2.7.0",
"@supabase/supabase-js": "^2.49.4",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/react-native": "13.2.0",
"@types/redux-mock-store": "^1.5.0",
"expo": "52.0.46",
"expo-dev-client": "~5.0.20",
"expo-fast-image": "1.1.3",
Expand All @@ -22,6 +27,7 @@
"expo-router": "4.0.20",
"expo-splash-screen": "0.29.24",
"expo-status-bar": "2.0.1",
"jest": "^29.7.0",
"metro-react-native-babel-preset": "^0.77.0",
"react": "18.3.1",
"react-dom": "18.3.1",
Expand All @@ -31,14 +37,17 @@
"react-native-reanimated": "3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "4.4.0",
"react-redux": "^9.2.0"
"react-redux": "^9.2.0",
"redux-mock-store": "^1.5.5",
"ts-jest": "^29.3.2"
},
"devDependencies": {
"@babel/core": "7.26.10",
"@trivago/prettier-plugin-sort-imports": "5.2.2",
"@types/react": "18.3.12",
"eas-cli": "16.3.3",
"prettier": "3.5.3",
"react-test-renderer": "18.3.1",
"typescript": "5.8.3"
},
"private": true
Expand Down
111 changes: 62 additions & 49 deletions src/components/settings/CalendarIntegration.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
import React from 'react';
import { View, Text, StyleSheet, Image, Switch } from 'react-native';
import { Button, Image, StyleSheet, Switch, Text, View } from 'react-native';

const CalendarIntegration = () => {
const [googleEnabled, setGoogleEnabled] = React.useState(false);
const [appleEnabled, setAppleEnabled] = React.useState(false);
interface CalendarIntegrationProps {
onIntegrate?: () => void;
}

return (
<View style={styles.container}>
<Text style={styles.sectionTitle}>Calendar Integration</Text>
<View style={styles.integrationItem}>
<Image
source={{ uri: 'https://www.gstatic.com/calendar/images/dynamiclogo_2020q4/calendar_31_2x.png' }}
style={styles.calendarIcon}
/>
<Text style={styles.integrationText}>Connect with Google Calendar</Text>
<Switch
value={googleEnabled}
onValueChange={setGoogleEnabled}
trackColor={{ false: '#767577', true: '#4285F4' }}
/>
</View>
const CalendarIntegration: React.FC<CalendarIntegrationProps> = ({
onIntegrate,
}) => {
const [googleEnabled, setGoogleEnabled] = React.useState(false);
const [appleEnabled, setAppleEnabled] = React.useState(false);

return (
<View style={styles.container}>
<Text style={styles.sectionTitle}>Calendar Integration</Text>
<View style={styles.integrationItem}>
<Image
source={{
uri: 'https://www.gstatic.com/calendar/images/dynamiclogo_2020q4/calendar_31_2x.png',
}}
style={styles.calendarIcon}
/>
<Text style={styles.integrationText}>Connect with Google Calendar</Text>
<Switch
value={googleEnabled}
onValueChange={setGoogleEnabled}
trackColor={{ false: '#767577', true: '#4285F4' }}
/>
</View>
<View style={styles.integrationItem}>
<Image
source={{ uri: 'https://help.apple.com/assets/65D689DF13D1B1E17703916F/65D689E0D302CF88600FDD25/en_US/941b3852f089696217cabe420c7a459f.png' }}
<Image
source={{
uri: 'https://help.apple.com/assets/65D689DF13D1B1E17703916F/65D689E0D302CF88600FDD25/en_US/941b3852f089696217cabe420c7a459f.png',
}}
style={styles.calendarIcon}
/>
<Text style={styles.integrationText}>Connect with Apple Calendar</Text>
Expand All @@ -32,37 +42,40 @@ const CalendarIntegration = () => {
trackColor={{ false: '#767577', true: '#007AFF' }}
/>
</View>
</View>
);
{onIntegrate && (
<Button title="Integrate Calendar" onPress={onIntegrate} />
)}
</View>
);
};

const styles = StyleSheet.create({
container: {
backgroundColor: '#F8F9FA',
borderRadius: 12,
marginHorizontal: 16,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 8,
color: '#333',
},
integrationItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
calendarIcon: {
width: 28,
height: 28,
marginRight: 12,
},
integrationText: {
flex: 1,
fontSize: 15,
color: '#333',
},
container: {
backgroundColor: '#F8F9FA',
borderRadius: 12,
marginHorizontal: 16,
},
sectionTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 8,
color: '#333',
},
integrationItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
calendarIcon: {
width: 28,
height: 28,
marginRight: 12,
},
integrationText: {
flex: 1,
fontSize: 15,
color: '#333',
},
});

export default CalendarIntegration;
8 changes: 7 additions & 1 deletion src/components/settings/ReminderSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import Modal from 'react-native-modal';

const ReminderSettings = () => {
interface ReminderSettingsProps {
onToggle?: () => void;
}

const ReminderSettings: React.FC<ReminderSettingsProps> = ({ onToggle }) => {
const [leadTime, setLeadTime] = useState('1 day before');
const [occasions, setOccasions] = useState('All occasions');
const [isLeadTimeModalVisible, setIsLeadTimeModalVisible] = useState(false);
Expand All @@ -14,10 +18,12 @@ const ReminderSettings = () => {

const toggleLeadTimeModal = () => {
setIsLeadTimeModalVisible(!isLeadTimeModalVisible);
if (onToggle) onToggle();
};

const toggleOccasionsModal = () => {
setIsOccasionsModalVisible(!isOccasionsModalVisible);
if (onToggle) onToggle();
};

return (
Expand Down
17 changes: 11 additions & 6 deletions src/components/settings/SyncedEvents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';

const SyncedEvents = () => {
const events = [
{ id: 1, name: 'Event name 1', date: '1 May 2025', synced: true },
{ id: 2, name: 'Event name 2', date: '12 Jan 2025', synced: false },
{ id: 3, name: 'Event name 3', date: '12 Dec 2024', synced: true },
];
interface Event {
id: number;
name: string;
date: string;
synced: boolean;
}

interface SyncedEventsProps {
events: Event[];
}

const SyncedEvents: React.FC<SyncedEventsProps> = ({ events }) => {
return (
<View style={styles.container}>
<Text style={styles.sectionTitle}>Synced Events</Text>
Expand Down
3 changes: 2 additions & 1 deletion src/features/gifts/giftService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ export const addGift = async (gift: CreateGiftDTO): Promise<GiftIdea> => {
return data as GiftIdea;
};

export const deleteGift = async (id: string): Promise<void> => {
export const deleteGift = async (id: string): Promise<boolean> => {
const { error } = await supabase.from('gifts').delete().eq('id', id);
if (error) throw error;
return true;
};

export const updateGift = async (
Expand Down
21 changes: 21 additions & 0 deletions tests/components/settings/CalendarIntegration.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { fireEvent, render } from '@testing-library/react-native';
import React from 'react';

import CalendarIntegration from '@/components/settings/CalendarIntegration';

describe('CalendarIntegration Component', () => {
it('should render the calendar integration button', () => {
const mockFn = jest.fn();
const { getByText } = render(<CalendarIntegration onIntegrate={mockFn} />);
expect(getByText('Integrate Calendar')).toBeTruthy();
});

it('should trigger calendar integration on button press', () => {
const mockIntegrateCalendar = jest.fn();
const { getByText } = render(
<CalendarIntegration onIntegrate={mockIntegrateCalendar} />,
);
fireEvent.press(getByText('Integrate Calendar'));
expect(mockIntegrateCalendar).toHaveBeenCalled();
});
});
20 changes: 20 additions & 0 deletions tests/components/settings/ReminderSettings.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { fireEvent, render } from '@testing-library/react-native';
import React from 'react';

import ReminderSettings from '@/components/settings/ReminderSettings';

describe('ReminderSettings Component', () => {
it('should render the reminder toggle', () => {
const { getByText } = render(<ReminderSettings />);
expect(getByText('Enable Reminders')).toBeTruthy();
Copy link

Copilot AI May 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ReminderSettings test is expecting a text 'Enable Reminders', but the updated component only renders a title such as 'Reminder Settings'. Please update the test assertion to match the current UI or modify the component accordingly.

Suggested change
expect(getByText('Enable Reminders')).toBeTruthy();
expect(getByText('Reminder Settings')).toBeTruthy();

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI May 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test expects the text 'Enable Reminders', but the ReminderSettings component now renders 'Reminder Settings' as the section title and no longer displays 'Enable Reminders'. Consider updating the test to reflect the current UI or restore the expected text in the component.

Suggested change
expect(getByText('Enable Reminders')).toBeTruthy();
expect(getByText('Reminder Settings')).toBeTruthy();

Copilot uses AI. Check for mistakes.
});

it('should toggle reminders on switch press', () => {
const mockToggleReminders = jest.fn();
const { getByText } = render(
<ReminderSettings onToggle={mockToggleReminders} />,
);
fireEvent.press(getByText('Enable Reminders'));
expect(mockToggleReminders).toHaveBeenCalled();
});
});
27 changes: 27 additions & 0 deletions tests/components/settings/SyncedEvents.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { fireEvent, render } from '@testing-library/react-native';
import React from 'react';

import SyncedEvents from '@/components/settings/SyncedEvents';

describe('SyncedEvents Component', () => {
it('should render a list of synced events', () => {
const events = [
{ id: 1, name: 'Event 1', date: '2025-05-01', synced: true },
{ id: 2, name: 'Event 2', date: '2025-05-03', synced: false },
];
const { getByText } = render(<SyncedEvents events={events} />);
expect(getByText('Event 1')).toBeTruthy();
expect(getByText('Event 2')).toBeTruthy();
});

it('should refresh events on button press', () => {
const events = [
{ id: 1, name: 'Event 1', date: '2025-05-01', synced: true },
{ id: 2, name: 'Event 2', date: '2025-05-03', synced: false },
];
const mockRefreshEvents = jest.fn();
const { getByText } = render(<SyncedEvents events={events} />);
Copy link

Copilot AI May 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SyncedEvents component does not render a button or text labeled 'Refresh Events', which causes a discrepancy between the test and component behavior. Please update the test or adjust the component to include this element if intended.

Suggested change
const { getByText } = render(<SyncedEvents events={events} />);
const { getByText } = render(<SyncedEvents events={events} refreshEvents={mockRefreshEvents} />);

Copilot uses AI. Check for mistakes.
fireEvent.press(getByText('Refresh Events'));
Comment on lines +23 to +24
Copy link

Copilot AI May 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test attempts to trigger a refresh action by pressing a 'Refresh Events' button, but the SyncedEvents component code does not render such a button. Consider updating the component to include a refresh control or modifying the test to match the current UI behavior.

Suggested change
const { getByText } = render(<SyncedEvents events={events} />);
fireEvent.press(getByText('Refresh Events'));
const { getByText } = render(<SyncedEvents events={events} onRefresh={mockRefreshEvents} />);
const refreshButton = getByText('Refresh Events');
expect(refreshButton).toBeTruthy();
fireEvent.press(refreshButton);

Copilot uses AI. Check for mistakes.
expect(mockRefreshEvents).toHaveBeenCalled();
});
});
Loading