├── 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 | --------------------------------------------------------------------------------