├── assets
├── images
│ ├── icon.png
│ ├── favicon.png
│ ├── post-it.png
│ ├── screen.png
│ ├── react-logo.png
│ ├── splash-icon.png
│ ├── adaptive-icon.png
│ ├── react-logo@2x.png
│ ├── react-logo@3x.png
│ └── partial-react-logo.png
└── fonts
│ └── SpaceMono-Regular.ttf
├── app
├── auth
│ ├── _layout.jsx
│ └── index.jsx
├── notes
│ ├── _layout.jsx
│ └── index.jsx
├── _layout.jsx
└── index.jsx
├── tsconfig.json
├── eas.json
├── components
├── NoteList.jsx
├── NoteItem.jsx
└── AddNoteModal.jsx
├── .gitignore
├── services
├── appwrite.js
├── authService.js
├── databaseService.js
└── noteService.js
├── README.md
├── app.json
├── contexts
└── AuthContext.js
└── package.json
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/notes-app/HEAD/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/notes-app/HEAD/assets/images/favicon.png
--------------------------------------------------------------------------------
/assets/images/post-it.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/notes-app/HEAD/assets/images/post-it.png
--------------------------------------------------------------------------------
/assets/images/screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/notes-app/HEAD/assets/images/screen.png
--------------------------------------------------------------------------------
/assets/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/notes-app/HEAD/assets/images/react-logo.png
--------------------------------------------------------------------------------
/assets/images/splash-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/notes-app/HEAD/assets/images/splash-icon.png
--------------------------------------------------------------------------------
/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/notes-app/HEAD/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/images/react-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/notes-app/HEAD/assets/images/react-logo@2x.png
--------------------------------------------------------------------------------
/assets/images/react-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/notes-app/HEAD/assets/images/react-logo@3x.png
--------------------------------------------------------------------------------
/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/notes-app/HEAD/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/assets/images/partial-react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradtraversy/notes-app/HEAD/assets/images/partial-react-logo.png
--------------------------------------------------------------------------------
/app/auth/_layout.jsx:
--------------------------------------------------------------------------------
1 | import { Stack } from 'expo-router';
2 |
3 | const AuthLayout = () => {
4 | return (
5 |
10 | );
11 | };
12 |
13 | export default AuthLayout;
14 |
--------------------------------------------------------------------------------
/app/notes/_layout.jsx:
--------------------------------------------------------------------------------
1 | import { Stack } from 'expo-router';
2 |
3 | const NoteLayout = () => {
4 | return (
5 |
10 | );
11 | };
12 |
13 | export default NoteLayout;
14 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "@/*": [
7 | "./*"
8 | ]
9 | }
10 | },
11 | "include": [
12 | "**/*.ts",
13 | "**/*.tsx",
14 | ".expo/types/**/*.ts",
15 | "expo-env.d.ts"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/eas.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli": {
3 | "version": ">= 15.0.12",
4 | "appVersionSource": "remote"
5 | },
6 | "build": {
7 | "development": {
8 | "developmentClient": true,
9 | "distribution": "internal"
10 | },
11 | "preview": {
12 | "distribution": "internal"
13 | },
14 | "production": {
15 | "autoIncrement": true
16 | }
17 | },
18 | "submit": {
19 | "production": {}
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/components/NoteList.jsx:
--------------------------------------------------------------------------------
1 | import { View, FlatList } from 'react-native';
2 | import NoteItem from './NoteItem';
3 |
4 | const NoteList = ({ notes, onDelete, onEdit }) => {
5 | return (
6 |
7 | item.$id}
10 | renderItem={({ item }) => (
11 |
12 | )}
13 | />
14 |
15 | );
16 | };
17 |
18 | export default NoteList;
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | .env
7 |
8 | # Expo
9 | .expo/
10 | dist/
11 | web-build/
12 | expo-env.d.ts
13 |
14 | # Native
15 | *.orig.*
16 | *.jks
17 | *.p8
18 | *.p12
19 | *.key
20 | *.mobileprovision
21 |
22 | # Metro
23 | .metro-health-check*
24 |
25 | # debug
26 | npm-debug.*
27 | yarn-debug.*
28 | yarn-error.*
29 |
30 | # macOS
31 | .DS_Store
32 | *.pem
33 |
34 | # local env files
35 | .env*.local
36 |
37 | # typescript
38 | *.tsbuildinfo
39 |
40 | app-example
41 |
--------------------------------------------------------------------------------
/services/appwrite.js:
--------------------------------------------------------------------------------
1 | import { Client, Databases, Account } from 'react-native-appwrite';
2 | import { Platform } from 'react-native';
3 |
4 | const config = {
5 | endpoint: process.env.EXPO_PUBLIC_APPWRITE_ENDPOINT,
6 | projectId: process.env.EXPO_PUBLIC_APPWRITE_PROJECT_ID,
7 | db: process.env.EXPO_PUBLIC_APPWRITE_DB_ID,
8 | col: {
9 | notes: process.env.EXPO_PUBLIC_APPWRITE_COL_NOTES_ID,
10 | },
11 | };
12 |
13 | const client = new Client()
14 | .setEndpoint(config.endpoint)
15 | .setProject(config.projectId);
16 |
17 | switch (Platform.OS) {
18 | case 'ios':
19 | client.setPlatform(process.env.EXPO_PUBLIC_APPWRITE_BUNDLE_ID);
20 | break;
21 | case 'android':
22 | client.setPlatform(process.env.EXPO_PUBLIC_APPWRITE_PACKAGE_NAME);
23 | break;
24 | }
25 |
26 | const database = new Databases(client);
27 |
28 | const account = new Account(client);
29 |
30 | export { database, config, client, account };
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Notes App (React Native)
2 |
3 | This is a simple notes app built with React Native. It allows users to create, read, update, and delete notes and store them using [Appwrite](https://apwr.dev/traversyfeb2025). It also uses authentication through Appwrite to allow users to sign up and log in and create private notes.
4 |
5 |
6 |
7 | This project goes with my React Native Mini-Course on YouTube.
8 |
9 | ## Usage
10 |
11 | Install the dependencies:
12 |
13 | ```bash
14 | npm install
15 | ```
16 |
17 | Rename the `.env.example` file to `.env` and fill in the required environment variables. You will need to sign into Appwrite [Here](https://apwr.dev/traversyfeb2025) and create a new project and database and fill in the required details in the `.env` file.
18 |
19 | Run the app:
20 |
21 | ```bash
22 | npm start
23 | ```
24 |
25 | You can then run the app on an emulator or on your phone using the Expo Go app.
26 |
27 | You can also use your browser by going to http://localhost:8081/.
28 |
29 | Use EAS(Expo Application Services) to build your app for Android and iOS.
30 |
31 | ```bash
32 | npm install -g eas-cli
33 | eas login
34 | eas init
35 |
36 | # For Android
37 | eas build --platform android
38 |
39 | # For iOS
40 | eas build --platform ios
41 | ```
42 |
--------------------------------------------------------------------------------
/services/authService.js:
--------------------------------------------------------------------------------
1 | import { account } from './appwrite';
2 | import { ID } from 'react-native-appwrite';
3 |
4 | const authService = {
5 | // Register a user
6 | async register(email, password) {
7 | try {
8 | const response = await account.create(ID.unique(), email, password);
9 | return response;
10 | } catch (error) {
11 | return {
12 | error: error.message || 'Registration failed. Please try agian',
13 | };
14 | }
15 | },
16 | // Login
17 | async login(email, password) {
18 | try {
19 | const response = await account.createEmailPasswordSession(
20 | email,
21 | password
22 | );
23 | return response;
24 | } catch (error) {
25 | return {
26 | error: error.message || 'Login failed. Please check your credentials',
27 | };
28 | }
29 | },
30 | // Get logged in user
31 | async getUser() {
32 | try {
33 | return await account.get();
34 | } catch (error) {
35 | return null;
36 | }
37 | },
38 |
39 | // Logout user
40 | async logout() {
41 | try {
42 | await account.deleteSession('current');
43 | } catch (error) {
44 | return {
45 | error: error.message || 'Logout failed. Please try again',
46 | };
47 | }
48 | },
49 | };
50 |
51 | export default authService;
52 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "notes-app",
4 | "slug": "notes-app",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/images/icon.png",
8 | "scheme": "myapp",
9 | "userInterfaceStyle": "automatic",
10 | "newArchEnabled": true,
11 | "ios": {
12 | "supportsTablet": true,
13 | "bundleIdentifier": "com.btraversy.notesapp",
14 | "infoPlist": {
15 | "ITSAppUsesNonExemptEncryption": false
16 | }
17 | },
18 | "android": {
19 | "adaptiveIcon": {
20 | "foregroundImage": "./assets/images/adaptive-icon.png",
21 | "backgroundColor": "#ffffff"
22 | },
23 | "package": "com.btraversy.notesapp"
24 | },
25 | "web": {
26 | "bundler": "metro",
27 | "output": "static",
28 | "favicon": "./assets/images/favicon.png"
29 | },
30 | "plugins": [
31 | "expo-router",
32 | [
33 | "expo-splash-screen",
34 | {
35 | "image": "./assets/images/splash-icon.png",
36 | "imageWidth": 200,
37 | "resizeMode": "contain",
38 | "backgroundColor": "#ffffff"
39 | }
40 | ]
41 | ],
42 | "experiments": {
43 | "typedRoutes": true
44 | },
45 | "extra": {
46 | "router": {
47 | "origin": false
48 | },
49 | "eas": {
50 | "projectId": "64919f94-3f8a-486c-b383-e5b2b63380ab"
51 | }
52 | },
53 | "owner": "btraversy"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/services/databaseService.js:
--------------------------------------------------------------------------------
1 | import { database } from './appwrite';
2 |
3 | const databaseService = {
4 | // List Documents
5 | async listDocuments(dbId, colId, queries = []) {
6 | try {
7 | const response = await database.listDocuments(dbId, colId, queries);
8 | return { data: response.documents || [], error: null };
9 | } catch (error) {
10 | console.error('Error fetching documents:', error.message);
11 | return { error: error.message };
12 | }
13 | },
14 | // Create Documents
15 | async createDocument(dbId, colId, data, id = null) {
16 | try {
17 | return await database.createDocument(dbId, colId, id || undefined, data);
18 | } catch (error) {
19 | console.error('Error creating document', error.message);
20 | return {
21 | error: error.message,
22 | };
23 | }
24 | },
25 | // Update Document
26 | async updateDocument(dbId, colId, id, data) {
27 | try {
28 | return await database.updateDocument(dbId, colId, id, data);
29 | } catch (error) {
30 | console.error('Error updating document', error.message);
31 | return {
32 | error: error.message,
33 | };
34 | }
35 | },
36 | // Delete Document
37 | async deleteDocument(dbId, colId, id) {
38 | try {
39 | await database.deleteDocument(dbId, colId, id);
40 | return { success: true };
41 | } catch (error) {
42 | console.error('Error deleting document', error.message);
43 | return {
44 | error: error.message,
45 | };
46 | }
47 | },
48 | };
49 |
50 | export default databaseService;
51 |
--------------------------------------------------------------------------------
/contexts/AuthContext.js:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useState, useEffect } from 'react';
2 | import authService from '../services/authService';
3 |
4 | const AuthContext = createContext();
5 |
6 | export const AuthProvider = ({ children }) => {
7 | const [user, setUser] = useState(null);
8 | const [loading, setLoading] = useState(true);
9 |
10 | useEffect(() => {
11 | checkUser();
12 | }, []);
13 |
14 | const checkUser = async () => {
15 | setLoading(true);
16 | const response = await authService.getUser();
17 |
18 | if (response?.error) {
19 | setUser(null);
20 | } else {
21 | setUser(response);
22 | }
23 |
24 | setLoading(false);
25 | };
26 |
27 | const login = async (email, password) => {
28 | const response = await authService.login(email, password);
29 |
30 | if (response?.error) {
31 | return response;
32 | }
33 |
34 | await checkUser();
35 | return { success: true };
36 | };
37 |
38 | const register = async (email, password) => {
39 | const response = await authService.register(email, password);
40 |
41 | if (response?.error) {
42 | return response;
43 | }
44 |
45 | return login(email, password); // Auto-login after register
46 | };
47 |
48 | const logout = async () => {
49 | await authService.logout();
50 | setUser(null);
51 | await checkUser();
52 | };
53 |
54 | return (
55 |
64 | {children}
65 |
66 | );
67 | };
68 |
69 | export const useAuth = () => useContext(AuthContext);
70 |
--------------------------------------------------------------------------------
/app/_layout.jsx:
--------------------------------------------------------------------------------
1 | import { Stack } from 'expo-router';
2 | import { AuthProvider, useAuth } from '@/contexts/AuthContext';
3 | import { TouchableOpacity, Text, StyleSheet } from 'react-native';
4 |
5 | const HeaderLogout = () => {
6 | const { user, logout } = useAuth();
7 |
8 | return user ? (
9 |
10 | Logout
11 |
12 | ) : null;
13 | };
14 |
15 | const RootLayout = () => {
16 | return (
17 |
18 | ,
29 | contentStyle: {
30 | paddingHorizontal: 10,
31 | paddingTop: 10,
32 | backgroundColor: '#fff',
33 | },
34 | }}
35 | >
36 |
37 |
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | const styles = StyleSheet.create({
45 | logoutButton: {
46 | marginRight: 15,
47 | paddingVertical: 5,
48 | paddingHorizontal: 10,
49 | backgroundColor: '#ff3b30',
50 | borderRadius: 8,
51 | },
52 | logoutText: {
53 | color: '#fff',
54 | fontSize: 16,
55 | fontWeight: 'bold',
56 | },
57 | });
58 |
59 | export default RootLayout;
60 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "notes-app",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "reset-project": "node ./scripts/reset-project.js",
8 | "android": "expo start --android",
9 | "ios": "expo start --ios",
10 | "web": "expo start --web",
11 | "test": "jest --watchAll",
12 | "lint": "expo lint"
13 | },
14 | "jest": {
15 | "preset": "jest-expo"
16 | },
17 | "dependencies": {
18 | "@expo/vector-icons": "^14.0.2",
19 | "@react-navigation/bottom-tabs": "^7.2.0",
20 | "@react-navigation/native": "^7.0.14",
21 | "expo": "~52.0.37",
22 | "expo-blur": "~14.0.3",
23 | "expo-constants": "~17.0.7",
24 | "expo-font": "~13.0.4",
25 | "expo-haptics": "~14.0.1",
26 | "expo-linking": "~7.0.5",
27 | "expo-router": "~4.0.17",
28 | "expo-splash-screen": "~0.29.22",
29 | "expo-status-bar": "~2.0.1",
30 | "expo-symbols": "~0.2.2",
31 | "expo-system-ui": "~4.0.8",
32 | "expo-web-browser": "~14.0.2",
33 | "react": "18.3.1",
34 | "react-dom": "18.3.1",
35 | "react-native": "0.76.7",
36 | "react-native-appwrite": "^0.7.0",
37 | "react-native-gesture-handler": "~2.20.2",
38 | "react-native-reanimated": "~3.16.1",
39 | "react-native-safe-area-context": "4.12.0",
40 | "react-native-screens": "~4.4.0",
41 | "react-native-url-polyfill": "^2.0.0",
42 | "react-native-web": "~0.19.13",
43 | "react-native-webview": "13.12.5"
44 | },
45 | "devDependencies": {
46 | "@babel/core": "^7.25.2",
47 | "@types/jest": "^29.5.12",
48 | "@types/react": "~18.3.12",
49 | "@types/react-test-renderer": "^18.3.0",
50 | "jest": "^29.2.1",
51 | "jest-expo": "~52.0.4",
52 | "react-test-renderer": "18.3.1",
53 | "typescript": "^5.3.3"
54 | },
55 | "private": true
56 | }
57 |
--------------------------------------------------------------------------------
/services/noteService.js:
--------------------------------------------------------------------------------
1 | import databaseService from './databaseService';
2 | import { ID, Query } from 'react-native-appwrite';
3 |
4 | // Appwrite database and collection id
5 | const dbId = process.env.EXPO_PUBLIC_APPWRITE_DB_ID;
6 | const colId = process.env.EXPO_PUBLIC_APPWRITE_COL_NOTES_ID;
7 |
8 | const noteService = {
9 | // Get Notes
10 | async getNotes(userId) {
11 | if (!userId) {
12 | console.error('Error: Missing userId in getNotes()');
13 | return {
14 | data: [],
15 | error: 'User ID is missing',
16 | };
17 | }
18 |
19 | try {
20 | const response = await databaseService.listDocuments(dbId, colId, [
21 | Query.equal('user_id', userId),
22 | ]);
23 | return response;
24 | } catch (error) {
25 | console.log('Error fetching notes:', error.message);
26 | return { data: [], error: error.message };
27 | }
28 | },
29 | // Add New Note
30 | async addNote(user_id, text) {
31 | if (!text) {
32 | return { error: 'Note text cannot be empty' };
33 | }
34 |
35 | const data = {
36 | text: text,
37 | createdAt: new Date().toISOString(),
38 | user_id: user_id,
39 | };
40 |
41 | const response = await databaseService.createDocument(
42 | dbId,
43 | colId,
44 | data,
45 | ID.unique()
46 | );
47 |
48 | if (response?.error) {
49 | return { error: response.error };
50 | }
51 |
52 | return { data: response };
53 | },
54 | // Update Note
55 | async updateNote(id, text) {
56 | const response = await databaseService.updateDocument(dbId, colId, id, {
57 | text,
58 | });
59 |
60 | if (response?.error) {
61 | return { error: response.error };
62 | }
63 |
64 | return { data: response };
65 | },
66 | // Delete Note
67 | async deleteNote(id) {
68 | const response = await databaseService.deleteDocument(dbId, colId, id);
69 | if (response?.error) {
70 | return { error: response.error };
71 | }
72 |
73 | return { success: true };
74 | },
75 | };
76 |
77 | export default noteService;
78 |
--------------------------------------------------------------------------------
/components/NoteItem.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useRef } from 'react';
2 | import {
3 | View,
4 | Text,
5 | StyleSheet,
6 | TouchableOpacity,
7 | TextInput,
8 | } from 'react-native';
9 |
10 | const NoteItem = ({ note, onDelete, onEdit }) => {
11 | const [isEditing, setIsEditing] = useState(false);
12 | const [editedText, setEditedText] = useState(note.text);
13 | const inputRef = useRef(null);
14 |
15 | const handleSave = () => {
16 | if (editedText.trim() === '') return;
17 | onEdit(note.$id, editedText);
18 | setIsEditing(false);
19 | };
20 |
21 | return (
22 |
23 | {isEditing ? (
24 |
33 | ) : (
34 | {note.text}
35 | )}
36 |
37 | {isEditing ? (
38 | {
40 | handleSave();
41 | inputRef.current?.blur();
42 | }}
43 | >
44 | 💾
45 |
46 | ) : (
47 | setIsEditing(true)}>
48 | ✏️
49 |
50 | )}
51 |
52 | onDelete(note.$id)}>
53 | ❌
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | const styles = StyleSheet.create({
61 | noteItem: {
62 | flexDirection: 'row',
63 | justifyContent: 'space-between',
64 | backgroundColor: '#f5f5f5',
65 | padding: 15,
66 | borderRadius: 5,
67 | marginVertical: 5,
68 | },
69 | noteText: {
70 | fontSize: 18,
71 | },
72 | delete: {
73 | fontSize: 18,
74 | color: 'red',
75 | },
76 | actions: {
77 | flexDirection: 'row',
78 | },
79 | edit: {
80 | fontSize: 18,
81 | marginRight: 10,
82 | color: 'blue',
83 | },
84 | });
85 |
86 | export default NoteItem;
87 |
--------------------------------------------------------------------------------
/app/index.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import {
3 | Text,
4 | View,
5 | StyleSheet,
6 | Image,
7 | TouchableOpacity,
8 | ActivityIndicator,
9 | } from 'react-native';
10 | import PostItImage from '@/assets/images/post-it.png';
11 | import { useRouter } from 'expo-router';
12 | import { useAuth } from '@/contexts/AuthContext';
13 |
14 | const HomeScreen = () => {
15 | const { user, loading } = useAuth();
16 | const router = useRouter();
17 |
18 | useEffect(() => {
19 | if (!loading && user) {
20 | router.replace('/notes');
21 | }
22 | }, [user, loading]);
23 |
24 | if (loading) {
25 | return (
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | return (
33 |
34 |
35 | Welcome To Notes App
36 |
37 | Capture your thoughts anytime, anywhere
38 |
39 |
40 | router.push('/notes')}
43 | >
44 | Get Started
45 |
46 |
47 | );
48 | };
49 |
50 | const styles = StyleSheet.create({
51 | container: {
52 | flex: 1,
53 | justifyContent: 'center',
54 | alignItems: 'center',
55 | padding: 20,
56 | backgroundColor: '#f8f9fa',
57 | },
58 | image: {
59 | width: 100,
60 | height: 100,
61 | marginBottom: 20,
62 | borderRadius: 10,
63 | },
64 | title: {
65 | fontSize: 28,
66 | fontWeight: 'bold',
67 | marginBottom: 10,
68 | color: '#333',
69 | },
70 | subtitle: {
71 | fontSize: 16,
72 | color: '#666',
73 | textAlign: 'center',
74 | marginBottom: 20,
75 | },
76 | button: {
77 | backgroundColor: '#007bff',
78 | paddingVertical: 12,
79 | paddingHorizontal: 25,
80 | borderRadius: 8,
81 | alignItems: 'center',
82 | },
83 | buttonText: {
84 | color: '#fff',
85 | fontSize: 18,
86 | fontWeight: 'bold',
87 | },
88 | centeredContainter: {
89 | alignItems: 'center',
90 | justifyContent: 'center',
91 | textAlign: 'center',
92 | },
93 | });
94 |
95 | export default HomeScreen;
96 |
--------------------------------------------------------------------------------
/components/AddNoteModal.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | View,
3 | Text,
4 | StyleSheet,
5 | TouchableOpacity,
6 | Modal,
7 | TextInput,
8 | } from 'react-native';
9 |
10 | const AddNoteModal = ({
11 | modalVisible,
12 | setModalVisible,
13 | newNote,
14 | setNewNote,
15 | addNote,
16 | }) => {
17 | return (
18 | setModalVisible(false)}
23 | >
24 |
25 |
26 | Add a New Note
27 |
34 |
35 | setModalVisible(false)}
38 | >
39 | Cancel
40 |
41 |
42 |
43 | Save
44 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | const styles = StyleSheet.create({
53 | modalOverlay: {
54 | flex: 1,
55 | backgroundColor: 'rgba(0,0,0,0.5)',
56 | justifyContent: 'center',
57 | alignItems: 'center',
58 | },
59 | modalContent: {
60 | backgroundColor: '#fff',
61 | padding: 20,
62 | borderRadius: 10,
63 | width: '80%',
64 | },
65 | modalTitle: {
66 | fontSize: 20,
67 | fontWeight: 'bold',
68 | marginBottom: 10,
69 | textAlign: 'center',
70 | },
71 | input: {
72 | borderWidth: 1,
73 | borderColor: '#ccc',
74 | borderRadius: 8,
75 | padding: 10,
76 | fontSize: 16,
77 | marginBottom: 15,
78 | },
79 | modalButtons: {
80 | flexDirection: 'row',
81 | justifyContent: 'space-between',
82 | },
83 | cancelButton: {
84 | backgroundColor: '#ccc',
85 | padding: 10,
86 | borderRadius: 5,
87 | flex: 1,
88 | marginRight: 10,
89 | alignItems: 'center',
90 | },
91 | cancelButtonText: {
92 | fontSize: 16,
93 | color: '#333',
94 | },
95 | saveButton: {
96 | backgroundColor: '#007bff',
97 | padding: 10,
98 | borderRadius: 5,
99 | flex: 1,
100 | alignItems: 'center',
101 | },
102 | saveButtonText: {
103 | fontSize: 16,
104 | color: '#fff',
105 | },
106 | });
107 |
108 | export default AddNoteModal;
109 |
--------------------------------------------------------------------------------
/app/auth/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import {
3 | View,
4 | Text,
5 | StyleSheet,
6 | TextInput,
7 | TouchableOpacity,
8 | Alert,
9 | } from 'react-native';
10 | import { useRouter } from 'expo-router';
11 | import { useAuth } from '@/contexts/AuthContext';
12 |
13 | const AuthScreen = () => {
14 | const { login, register } = useAuth();
15 | const router = useRouter();
16 | const [email, setEmail] = useState('');
17 | const [password, setPassword] = useState('');
18 | const [confirmPassword, setConfirmPassword] = useState('');
19 | const [isRegistering, setIsRegistering] = useState(false);
20 | const [error, setError] = useState(false);
21 |
22 | const handleAuth = async () => {
23 | if (!email.trim() || !password.trim()) {
24 | setError('Email and password are required');
25 | return;
26 | }
27 |
28 | if (isRegistering && password !== confirmPassword) {
29 | setError('Passwords do not match');
30 | return;
31 | }
32 |
33 | let response;
34 |
35 | if (isRegistering) {
36 | response = await register(email, password);
37 | } else {
38 | response = await login(email, password);
39 | }
40 |
41 | if (response?.error) {
42 | Alert.alert('Error', response.error);
43 | return;
44 | }
45 |
46 | router.replace('/notes');
47 | };
48 |
49 | return (
50 |
51 | {isRegistering ? 'Sign Up' : 'Login'}
52 |
53 | {error ? {error} : null}
54 |
55 |
64 |
65 |
74 |
75 | {isRegistering && (
76 |
85 | )}
86 |
87 |
88 |
89 | {isRegistering ? 'Sign Up' : 'Login'}
90 |
91 |
92 |
93 | setIsRegistering(!isRegistering)}>
94 |
95 | {isRegistering
96 | ? 'Already have an account? Login'
97 | : "Don't have an account? Sign Up"}
98 |
99 |
100 |
101 | );
102 | };
103 |
104 | const styles = StyleSheet.create({
105 | container: {
106 | flex: 1,
107 | justifyContent: 'center',
108 | alignItems: 'center',
109 | padding: 20,
110 | backgroundColor: '#f8f9fa',
111 | },
112 | header: {
113 | fontSize: 28,
114 | fontWeight: 'bold',
115 | marginBottom: 20,
116 | color: '#333',
117 | },
118 | input: {
119 | width: '100%',
120 | padding: 12,
121 | borderWidth: 1,
122 | borderColor: '#ddd',
123 | borderRadius: 8,
124 | marginBottom: 12,
125 | backgroundColor: '#fff',
126 | fontSize: 16,
127 | },
128 | button: {
129 | backgroundColor: '#007bff',
130 | paddingVertical: 12,
131 | borderRadius: 8,
132 | width: '100%',
133 | alignItems: 'center',
134 | marginTop: 10,
135 | },
136 | buttonText: {
137 | color: '#fff',
138 | fontSize: 18,
139 | fontWeight: 'bold',
140 | },
141 | switchText: {
142 | marginTop: 10,
143 | color: '#007bff',
144 | fontSize: 16,
145 | },
146 | error: {
147 | color: 'red',
148 | marginBottom: 10,
149 | fontSize: 16,
150 | },
151 | });
152 |
153 | export default AuthScreen;
154 |
--------------------------------------------------------------------------------
/app/notes/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import {
3 | View,
4 | Text,
5 | StyleSheet,
6 | TouchableOpacity,
7 | Alert,
8 | ActivityIndicator,
9 | } from 'react-native';
10 | import { useRouter } from 'expo-router';
11 | import { useAuth } from '@/contexts/AuthContext';
12 | import NoteList from '@/components/NoteList';
13 | import AddNoteModal from '@/components/AddNoteModal';
14 | import noteService from '@/services/noteService';
15 |
16 | const NoteScreen = () => {
17 | const router = useRouter();
18 | const { user, loading: authLoading } = useAuth();
19 |
20 | const [notes, setNotes] = useState([]);
21 | const [modalVisible, setModalVisible] = useState(false);
22 | const [newNote, setNewNote] = useState('');
23 | const [loading, setLoading] = useState(true);
24 | const [error, setError] = useState(null);
25 |
26 | useEffect(() => {
27 | if (!authLoading && !user) {
28 | router.replace('/auth');
29 | }
30 | }, [user, authLoading]);
31 |
32 | useEffect(() => {
33 | if (user) {
34 | fetchNotes();
35 | }
36 | }, [user]);
37 |
38 | const fetchNotes = async () => {
39 | setLoading(true);
40 | const response = await noteService.getNotes(user.$id);
41 |
42 | if (response.error) {
43 | setError(response.error);
44 | Alert.alert('Error', response.error);
45 | } else {
46 | setNotes(response.data);
47 | setError(null);
48 | }
49 |
50 | setLoading(false);
51 | };
52 |
53 | // Add New Note
54 | const addNote = async () => {
55 | if (newNote.trim() === '') return;
56 |
57 | const response = await noteService.addNote(user.$id, newNote);
58 |
59 | if (response.error) {
60 | Alert.alert('Error', response.error);
61 | } else {
62 | setNotes([...notes, response.data]);
63 | }
64 |
65 | setNewNote('');
66 | setModalVisible(false);
67 | };
68 |
69 | // Delete Note
70 | const deleteNote = async (id) => {
71 | Alert.alert('Delete Note', 'Are you sure you want to delete this note?', [
72 | {
73 | text: 'Cancel',
74 | style: 'cancel',
75 | },
76 | {
77 | text: 'Delete',
78 | style: 'destructive',
79 | onPress: async () => {
80 | const response = await noteService.deleteNote(id);
81 | if (response.error) {
82 | Alert.alert('Error', response.error);
83 | } else {
84 | setNotes(notes.filter((note) => note.$id !== id));
85 | }
86 | },
87 | },
88 | ]);
89 | };
90 |
91 | // Edit Note
92 | const editNote = async (id, newText) => {
93 | if (!newText.trim()) {
94 | Alert.alert('Error', 'Note text cannot be empty');
95 | return;
96 | }
97 |
98 | const response = await noteService.updateNote(id, newText);
99 | if (response.error) {
100 | Alert.alert('Error', response.error);
101 | } else {
102 | setNotes((prevNotes) =>
103 | prevNotes.map((note) =>
104 | note.$id === id ? { ...note, text: response.data.text } : note
105 | )
106 | );
107 | }
108 | };
109 |
110 | return (
111 |
112 | {loading ? (
113 |
114 | ) : (
115 | <>
116 | {error && {error}}
117 |
118 | {notes.length === 0 ? (
119 | You have no notes
120 | ) : (
121 |
122 | )}
123 | >
124 | )}
125 |
126 | setModalVisible(true)}
129 | >
130 | + Add Note
131 |
132 |
133 | {/* Modal */}
134 |
141 |
142 | );
143 | };
144 |
145 | const styles = StyleSheet.create({
146 | container: {
147 | flex: 1,
148 | padding: 20,
149 | backgroundColor: '#fff',
150 | },
151 | addButton: {
152 | position: 'absolute',
153 | bottom: 20,
154 | left: 20,
155 | right: 20,
156 | backgroundColor: '#007bff',
157 | padding: 15,
158 | borderRadius: 8,
159 | alignItems: 'center',
160 | },
161 | addButtonText: {
162 | color: '#fff',
163 | fontSize: 18,
164 | fontWeight: 'bold',
165 | },
166 | errorText: {
167 | color: 'red',
168 | textAlign: 'center',
169 | marginBottom: 10,
170 | fontSize: 16,
171 | },
172 | noNotesText: {
173 | textAlign: 'center',
174 | fontSize: 18,
175 | fontWeight: 'bold',
176 | color: '#555',
177 | marginTop: 15,
178 | },
179 | });
180 |
181 | export default NoteScreen;
182 |
--------------------------------------------------------------------------------