├── .eslintrc ├── .expo-shared └── assets.json ├── .gitignore ├── App.js ├── AppNavigator.js ├── Notifications ├── cancelNotification.js ├── scheduleReminderNotification.js └── useNotifications.js ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── components ├── ActionButton.js ├── FolderCard.js ├── Modal.js ├── NoteCard.js ├── NoteCardText.js ├── NoteOptionsActionSheet.js ├── NoteReminderModal.js ├── Notes.js └── NotesPageHeader.js ├── context └── context.jsx ├── eas.json ├── package-lock.json ├── screens ├── AddNote.js ├── FolderNotes.js ├── Folders.js ├── Home.js ├── LabelsManager.js ├── NoteLabelsManager.js ├── NotesSelector.js ├── Trash.js ├── UpdateNote.js └── index.js ├── style └── theme.js └── utils ├── dateformat.js └── storage.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "react-native" 4 | ], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:react/recommended", 8 | "plugin:react-native/all", 9 | "plugin:react-hooks/recommended" 10 | ], 11 | "parser": "@babel/eslint-parser", 12 | "env": { 13 | "es6": true, 14 | "react-native/react-native": true 15 | }, 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "jsx": true 19 | }, 20 | "ecmaVersion": "latest", 21 | "sourceType": "module" 22 | }, 23 | "rules": { 24 | "react-native/no-unused-styles": 0, 25 | "react-native/sort-styles": 0, 26 | "react-native/split-platform-components": 0, 27 | "react-native/no-inline-styles": 0, 28 | "react-native/no-color-literals": 0, 29 | "react-native/no-raw-text": 2, 30 | "react/jsx-uses-react": "warn", 31 | "react/jsx-uses-vars": "warn", 32 | "no-unused-vars": "warn", 33 | "react/prop-types": 0, 34 | "react-hooks/rules-of-hooks": "warn", 35 | "react-hooks/exhaustive-deps": "warn" 36 | } 37 | } -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import { GlobalContextProvider } from './context/context'; 2 | import React from 'react' 3 | import useNotifications from './Notifications/useNotifications'; 4 | import { Snackbar } from '@ouroboros/react-native-snackbar'; 5 | import AppNavigator from './AppNavigator'; 6 | import { View } from 'react-native'; 7 | import { StatusBar } from 'expo-status-bar'; 8 | 9 | export default function App() { 10 | const navRef = useNotifications(); 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | ); 23 | } -------------------------------------------------------------------------------- /AppNavigator.js: -------------------------------------------------------------------------------- 1 | import { NavigationContainer } from '@react-navigation/native'; 2 | import * as screens from './screens'; 3 | import React from 'react' 4 | import { createDrawerNavigator, DrawerItemList } from '@react-navigation/drawer'; 5 | import { SafeAreaView } from 'react-native-safe-area-context'; 6 | import { ScrollView, Text } from 'react-native'; 7 | import { createSharedElementStackNavigator } from 'react-navigation-shared-element'; 8 | import { CardStyleInterpolators } from '@react-navigation/stack'; 9 | 10 | const Stack = createSharedElementStackNavigator(); 11 | const Drawer = createDrawerNavigator(); 12 | 13 | const DrawerContent = (props) => { 14 | return ( 15 | <> 16 | 17 | 18 | Notes App 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | const AppNavigator = ({ navRef }) => { 27 | return ( 28 | 29 | 32 | ( 33 | 34 | 35 | 36 | 37 | 38 | 39 | )} /> 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | ) 49 | } 50 | 51 | export default AppNavigator -------------------------------------------------------------------------------- /Notifications/cancelNotification.js: -------------------------------------------------------------------------------- 1 | import * as Notifications from 'expo-notifications'; 2 | import { Alert } from 'react-native'; 3 | 4 | export async function cancelNotification(notifId) { 5 | try { 6 | await Notifications.cancelScheduledNotificationAsync(notifId); 7 | } 8 | catch (err) { 9 | Alert.alert("Error!") 10 | } 11 | } -------------------------------------------------------------------------------- /Notifications/scheduleReminderNotification.js: -------------------------------------------------------------------------------- 1 | import * as Notifications from 'expo-notifications'; 2 | import { Alert } from 'react-native'; 3 | 4 | export default async function scheduleReminderNotification({ note, dateTime }) { 5 | try { 6 | const id = await Notifications.scheduleNotificationAsync({ 7 | content: { 8 | title: "Reminder", 9 | body: note.text.slice(0, 80) + "...", 10 | data: { navigateTo: "UpdateNote", params: { id: note.id } } 11 | }, 12 | trigger: dateTime 13 | }); 14 | return id; 15 | } 16 | catch (err) { 17 | console.log(err); 18 | Alert.alert("error"); 19 | } 20 | } -------------------------------------------------------------------------------- /Notifications/useNotifications.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import * as Notifications from 'expo-notifications'; 3 | import { Platform } from "react-native"; 4 | 5 | Notifications.setNotificationHandler({ 6 | handleNotification: async () => ({ 7 | shouldShowAlert: true, 8 | shouldPlaySound: true, 9 | shouldSetBadge: false, 10 | }), 11 | }); 12 | 13 | const useNotifications = () => { 14 | // const [expoPushToken, setExpoPushToken] = useState(''); 15 | const [notification, setNotification] = useState(false); 16 | const notificationListener = useRef(); 17 | const responseListener = useRef(); 18 | const navRef = useRef(); 19 | 20 | useEffect(() => { 21 | // registerForPushNotificationsAsync().then(token => setExpoPushToken(token)); 22 | 23 | // This listener is fired whenever a notification is received while the app is foregrounded 24 | notificationListener.current = Notifications.addNotificationReceivedListener(notification => { 25 | setNotification(notification); 26 | }); 27 | 28 | // This listener is fired whenever a user taps on or interacts with a notification (works when app is foregrounded, backgrounded, or killed) 29 | responseListener.current = Notifications.addNotificationResponseReceivedListener(response => { 30 | const { data } = response.notification.request.content; 31 | let { navigateTo, params } = data; 32 | navigateTo = navigateTo || "Home"; 33 | navRef.current.navigate(navigateTo, params); 34 | }); 35 | 36 | return () => { 37 | Notifications.removeNotificationSubscription(notificationListener.current); 38 | Notifications.removeNotificationSubscription(responseListener.current); 39 | }; 40 | }, []); 41 | 42 | return navRef; 43 | 44 | } 45 | 46 | 47 | async function registerForPushNotificationsAsync() { 48 | let token; 49 | const isDevice = true; 50 | if (isDevice) { 51 | const { status: existingStatus } = await Notifications.getPermissionsAsync(); 52 | let finalStatus = existingStatus; 53 | if (existingStatus !== 'granted') { 54 | const { status } = await Notifications.requestPermissionsAsync(); 55 | finalStatus = status; 56 | } 57 | if (finalStatus !== 'granted') { 58 | alert('Failed to get push token for push notification!'); 59 | return; 60 | } 61 | token = (await Notifications.getExpoPushTokenAsync()).data; 62 | console.log(token); 63 | } else { 64 | alert('Must use physical device for Push Notifications'); 65 | } 66 | 67 | if (Platform.OS === 'android') { 68 | Notifications.setNotificationChannelAsync('default', { 69 | name: 'default', 70 | importance: Notifications.AndroidImportance.MAX, 71 | vibrationPattern: [0, 250, 250, 250], 72 | lightColor: '#FF231F7C', 73 | }); 74 | } 75 | 76 | return token; 77 | } 78 | 79 | export default useNotifications; 80 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Notes app", 4 | "slug": "react-native-notes-app", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "updates": { 15 | "fallbackToCacheTimeout": 0 16 | }, 17 | "assetBundlePatterns": [ 18 | "**/*" 19 | ], 20 | "ios": { 21 | "supportsTablet": true 22 | }, 23 | "android": { 24 | "package": "com.notes", 25 | "versionCode": 1, 26 | "adaptiveIcon": { 27 | "foregroundImage": "./assets/adaptive-icon.png", 28 | "backgroundColor": "#1d9bff" 29 | } 30 | }, 31 | "web": { 32 | "favicon": "./assets/favicon.png" 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/react-native-notes-app/685a0b90541dda5c84fbd8d1aac04942dc08ce2e/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/react-native-notes-app/685a0b90541dda5c84fbd8d1aac04942dc08ce2e/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/react-native-notes-app/685a0b90541dda5c84fbd8d1aac04942dc08ce2e/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/koatane-dev/react-native-notes-app/685a0b90541dda5c84fbd8d1aac04942dc08ce2e/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: [ 6 | "react-native-reanimated/plugin", 7 | ], 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /components/ActionButton.js: -------------------------------------------------------------------------------- 1 | import { Pressable, StyleSheet } from 'react-native' 2 | import React from 'react' 3 | import theme from '../style/theme'; 4 | import Icon from 'react-native-vector-icons/Ionicons'; 5 | import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'; 6 | const AnimatedPressable = Animated.createAnimatedComponent(Pressable); 7 | 8 | const ActionButton = ({ style = {}, onPress, iconName = "add-outline", isVisible = true }) => { 9 | const animatedStyles = useAnimatedStyle(() => ({ 10 | transform: [{ scale: withTiming(isVisible ? 1 : 0, { duration: 200 }) }] 11 | }), [isVisible]); 12 | 13 | return ( 14 | 15 | 16 | 17 | ) 18 | } 19 | 20 | export default ActionButton 21 | 22 | const styles = StyleSheet.create({ 23 | buttonStyle: { 24 | position: "absolute", 25 | bottom: 20, 26 | right: 20, 27 | width: 50, 28 | height: 50, 29 | backgroundColor: theme.PRIMARY_COLOR, 30 | color: 'white', 31 | borderRadius: 50, 32 | justifyContent: "center", 33 | alignItems: "center", 34 | } 35 | }) 36 | 37 | -------------------------------------------------------------------------------- /components/FolderCard.js: -------------------------------------------------------------------------------- 1 | import { View, Text, Pressable } from 'react-native' 2 | import React from 'react' 3 | import { convertToXTimeAgo } from '../utils/dateformat' 4 | import Icon from 'react-native-vector-icons/Ionicons'; 5 | import theme from '../style/theme'; 6 | 7 | const FolderCard = ({ folder, drag, isActive, handlePress }) => { 8 | return ( 9 | 20 | handlePress(folder.id)} 22 | onLongPress={() => drag()} 23 | android_ripple={{ color: "#bbb", radius: 200 }} 24 | delayLongPress={100} 25 | > 26 | 27 | 28 | {folder.name} 29 | 30 | {folder.notes.length} note{folder.notes.length != 1 && 's'} 31 | {convertToXTimeAgo(folder.createdAt)} 32 | 33 | 34 | 35 | 36 | 37 | 38 | ) 39 | } 40 | 41 | export default FolderCard -------------------------------------------------------------------------------- /components/Modal.js: -------------------------------------------------------------------------------- 1 | import { View, Modal as RNModal, Pressable, StyleSheet } from 'react-native' 2 | import React from 'react' 3 | 4 | const Modal = ({ children, visible, onRequestClose, style = {} }) => { 5 | return ( 6 | 7 | e.target == e.currentTarget && onRequestClose()}> 8 | 9 | {children} 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | export default Modal 17 | 18 | 19 | const styles = StyleSheet.create({ 20 | centeredView: { 21 | flex: 1, 22 | justifyContent: "center", 23 | alignItems: "center", 24 | backgroundColor: "rgba(0, 0, 0, 0.2)" 25 | }, 26 | modalView: { 27 | backgroundColor: "white", 28 | borderRadius: 5, 29 | padding: 10, 30 | width: "80%", 31 | shadowColor: "#000", 32 | shadowOffset: { 33 | width: 0, 34 | height: 2 35 | }, 36 | shadowOpacity: 0.25, 37 | shadowRadius: 4, 38 | elevation: 5 39 | } 40 | }) -------------------------------------------------------------------------------- /components/NoteCard.js: -------------------------------------------------------------------------------- 1 | import { Pressable, StyleSheet, View, Dimensions } from 'react-native' 2 | import React from 'react' 3 | import { PanGestureHandler, GestureHandlerRootView } from 'react-native-gesture-handler'; 4 | import Animated, { runOnJS, useAnimatedGestureHandler, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; 5 | import theme from '../style/theme'; 6 | import NoteCardText from './NoteCardText'; 7 | import { SharedElement } from 'react-navigation-shared-element'; 8 | 9 | // "isActive" is used in the situation of dragging the note and to check if currently it is being dragged 10 | const NoteCard = ({ note, isActive = false, onPress = () => { }, onLongPress = () => { }, moveNoteToTrash, isAddedInSelection = false }) => { 11 | 12 | const { width: SCREEN_WIDTH } = Dimensions.get("window"); 13 | const TRANSLATE_X_THRESHOLD = -SCREEN_WIDTH * 0.4; 14 | const translateX = useSharedValue(0); 15 | 16 | const panGesture = useAnimatedGestureHandler({ 17 | onActive: e => { 18 | translateX.value = e.translationX; 19 | }, 20 | onEnd: () => { 21 | const shouldBeRemoved = translateX.value < TRANSLATE_X_THRESHOLD; 22 | if (shouldBeRemoved) { 23 | translateX.value = withTiming(-SCREEN_WIDTH, { duration: 100 }, (isFinished) => { 24 | if (isFinished) runOnJS(moveNoteToTrash)(note.id); 25 | }); 26 | } else { 27 | translateX.value = withTiming(0); 28 | } 29 | } 30 | }); 31 | 32 | const rStyle = useAnimatedStyle(() => ({ 33 | transform: [{ 34 | translateX: translateX.value, 35 | }, { 36 | scale: withTiming(isActive ? 1.04 : 1, { duration: 100 }) 37 | }] 38 | })); 39 | 40 | 41 | return ( 42 | 43 | 44 | 45 | 46 | 47 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | 64 | 65 | export default NoteCard 66 | 67 | 68 | const styles = ({ isAddedInSelection }) => StyleSheet.create({ 69 | noteCard: [{ 70 | borderRadius: 5, 71 | backgroundColor: "#fff", 72 | borderWidth: 2, 73 | borderColor: "#fff" 74 | }, 75 | isAddedInSelection ? { 76 | borderColor: theme.PRIMARY_COLOR, 77 | } : { 78 | borderBottomColor: "#e4e4e4", 79 | }] 80 | }) -------------------------------------------------------------------------------- /components/NoteCardText.js: -------------------------------------------------------------------------------- 1 | import { View, Text, ScrollView } from 'react-native' 2 | import React from 'react' 3 | import Icon from 'react-native-vector-icons/Ionicons'; 4 | import { convertToXTimeAgo } from '../utils/dateformat'; 5 | import moment from 'moment'; 6 | 7 | const NoteCardText = ({ note }) => { 8 | const isReminderTimePassed = new Date(note.reminder?.dateTime).getTime() < new Date().getTime(); 9 | 10 | return ( 11 | 12 | 13 | {note.color && ( 14 | 15 | )} 16 | {convertToXTimeAgo(note.updatedAt)} 17 | {note.isBookmarked && ( 18 | 19 | )} 20 | {note.isPinned && ( 21 | 22 | )} 23 | 24 | 25 | {note.labels.length > 0 && ( 26 | 27 | {note.labels?.map(label => ( 28 | {label} 29 | ))} 30 | 31 | )} 32 | 33 | {note.reminder?.dateTime && ( 34 | 35 | 36 | 37 | {moment(note.reminder?.dateTime).format("lll")} 38 | 39 | 40 | )} 41 | 42 | {note.text} 43 | 44 | ) 45 | } 46 | 47 | export default NoteCardText -------------------------------------------------------------------------------- /components/NoteOptionsActionSheet.js: -------------------------------------------------------------------------------- 1 | import { View, Text, StyleSheet, ScrollView, Pressable, ToastAndroid, Alert, Share } from 'react-native' 2 | import React, { useState } from 'react' 3 | import ActionSheet, { SheetManager } from "react-native-actions-sheet"; 4 | import Icon from 'react-native-vector-icons/Ionicons'; 5 | import { getData, storeData } from '../utils/storage'; 6 | import * as Clipboard from 'expo-clipboard'; 7 | import { useNavigation } from '@react-navigation/native'; 8 | import { useGlobalContext } from '../context/context'; 9 | import NoteReminderModal from './NoteReminderModal'; 10 | import { addMessage } from '@ouroboros/react-native-snackbar'; 11 | 12 | const NoteOptionsActionSheet = ({ noteId, formData }) => { 13 | const navigation = useNavigation(); 14 | const { notes, setNotes } = useGlobalContext(); 15 | const [noteReminderModal, setNoteReminderModal] = useState(false); 16 | 17 | const note = notes.find(note => note.id === noteId); 18 | const noteColors = ["lightseagreen", "skyblue", "lightcoral", "lightpink", "lightgreen", "lightblue", "orange", "palegreen"]; 19 | const newOrEditMode = note.reminder?.dateTime ? "edit" : "new"; 20 | 21 | const changeNoteColor = async color => { 22 | try { 23 | const newNotesArr = notes.map(note => { 24 | if (note.id !== noteId) return note; 25 | return { ...note, color }; 26 | }) 27 | await storeData("notes", newNotesArr); 28 | setNotes(newNotesArr); 29 | } 30 | catch (err) { 31 | Alert.alert("Error", "Some error is there!!"); 32 | } 33 | } 34 | 35 | const copyToClipboard = async () => { 36 | await Clipboard.setStringAsync(formData.text); 37 | ToastAndroid.show("Note's text copied", ToastAndroid.SHORT); 38 | } 39 | 40 | const shareNote = async () => { 41 | await Share.share({ message: formData.text }); 42 | } 43 | 44 | const restoreNote = async () => { 45 | try { 46 | const trashNotes = await getData("trashNotes"); 47 | const note = trashNotes.find(note => note.id === noteId); 48 | const newNotesArr = [...notes]; 49 | await storeData("notes", newNotesArr); 50 | 51 | const newTrashNotesArr = trashNotes.filter(trashNote => trashNote.id !== note.id); 52 | await storeData("trashNotes", newTrashNotesArr); 53 | setNotes(newNotesArr); 54 | } 55 | catch (err) { 56 | Alert.alert("Error", "Some error is there!!"); 57 | } 58 | } 59 | 60 | 61 | const moveNoteToTrash = async () => { 62 | try { 63 | const trashNotes = (await getData("trashNotes")) || []; 64 | const newTrashNotesArr = [...trashNotes, note]; 65 | await storeData("trashNotes", newTrashNotesArr); 66 | 67 | const newNotesArr = notes.filter(note => note.id !== noteId); 68 | await storeData("notes", newNotesArr); 69 | navigation.navigate("Home"); 70 | setNotes(newNotesArr); 71 | 72 | addMessage({ 73 | text: "Note moved to trash", 74 | duration: 5000, 75 | action: { 76 | text: "Undo", 77 | onPress: restoreNote 78 | } 79 | }); 80 | 81 | } 82 | catch (err) { 83 | Alert.alert("Error", "Some error is there!!"); 84 | } 85 | } 86 | 87 | 88 | const cloneNote = async () => { 89 | try { 90 | const newNote = { 91 | ...note, 92 | id: Math.floor(Math.random() * 10000), 93 | text: formData.text, 94 | createdAt: new Date(), 95 | updatedAt: new Date(), 96 | } 97 | const newNotesArr = [newNote, ...notes]; 98 | await storeData("notes", newNotesArr); 99 | setNotes(newNotesArr); 100 | navigation.navigate("Home"); 101 | ToastAndroid.show("Note cloned successfully", ToastAndroid.SHORT); 102 | } 103 | catch (err) { 104 | Alert.alert("Error", "Some error is there!!"); 105 | } 106 | } 107 | 108 | const togglePin = async () => { 109 | try { 110 | const newNotesArr = notes.map(note => { 111 | if (note.id !== noteId) return note; 112 | return { ...note, isPinned: !note.isPinned }; 113 | }) 114 | await storeData("notes", newNotesArr); 115 | setNotes(newNotesArr); 116 | } 117 | catch (err) { 118 | Alert.alert("Error", "Some error is there!!"); 119 | } 120 | } 121 | 122 | const showAddReminderModal = () => { 123 | SheetManager.hide("noteOptionsActionSheet"); 124 | setNoteReminderModal(true); 125 | } 126 | 127 | 128 | 129 | return ( 130 | <> 131 | 132 | 133 | 134 | {/* Colors */} 135 | 136 | changeNoteColor(null)} style={{ marginHorizontal: 5, borderRadius: 50, width: 35, height: 35, backgroundColor: "white", justifyContent: "center", alignItems: "center", borderWidth: 1, borderColor: "black" }}> 137 | 138 | {!note.color && ( 139 | 140 | )} 141 | 142 | 143 | {noteColors.map(color => ( 144 | changeNoteColor(color)} key={color} style={{ marginHorizontal: 5, borderRadius: 50, width: 35, height: 35, backgroundColor: color, justifyContent: "center", alignItems: "center" }}> 145 | {note.color === color && ( 146 | 147 | )} 148 | 149 | ))} 150 | 151 | 152 | 153 | 154 | {/* Labels */} 155 | 156 | {note.labels?.map(label => ( 157 | {label} 158 | ))} 159 | { SheetManager.hide("noteOptionsActionSheet"); navigation.navigate("NoteLabels", { noteId }); }} style={{ marginLeft: 14, backgroundColor: "#eee", paddingVertical: 5, paddingHorizontal: 8, borderRadius: 3 }} android_ripple={{ color: "#bbb", radius: 200 }} > 160 | + Manage labels 161 | 162 | 163 | 164 | 165 | 166 | {[ 167 | { onPress: copyToClipboard, iconName: "clipboard-outline", title: "Copy to Clipboard" }, 168 | { onPress: shareNote, iconName: "share-social-outline", title: "Share" }, 169 | { onPress: moveNoteToTrash, iconName: "trash-outline", title: "Delete" }, 170 | { onPress: cloneNote, iconName: "copy-outline", title: "Make a copy" }, 171 | { onPress: togglePin, iconName: note.isPinned ? "pin" : "pin-outline", title: note.isPinned ? "Unpin" : "Pin" }, 172 | { onPress: showAddReminderModal, iconName: "alarm-outline", title: newOrEditMode === "edit" ? "Edit reminder" : "Add a reminder" }, 173 | ] 174 | .map(({ onPress, iconName, title }) => ( 175 | 176 | 177 | {title} 178 | 179 | ))} 180 | 181 | 182 | 183 | 184 | 185 | ) 186 | } 187 | 188 | export default NoteOptionsActionSheet 189 | 190 | const styles = StyleSheet.create({ 191 | sheetItem: { 192 | flexDirection: "row", 193 | alignItems: "center", 194 | padding: 15, 195 | } 196 | }); 197 | -------------------------------------------------------------------------------- /components/NoteReminderModal.js: -------------------------------------------------------------------------------- 1 | import { Alert, Pressable, Text, View } from 'react-native' 2 | import React, { useEffect, useState } from 'react' 3 | import RNDateTimePicker from '@react-native-community/datetimepicker'; 4 | import scheduleReminderNotification from '../Notifications/scheduleReminderNotification'; 5 | import moment from 'moment/moment'; 6 | import { useGlobalContext } from '../context/context'; 7 | import { storeData } from '../utils/storage'; 8 | import { cancelNotification } from '../Notifications/cancelNotification'; 9 | import Modal from './Modal'; 10 | 11 | const NoteReminderModal = ({ noteReminderModal, setNoteReminderModal, noteId }) => { 12 | const { notes, setNotes } = useGlobalContext(); 13 | const [dateTimePickerMode, setDateTimePickerMode] = useState(null); 14 | const [inputDateTime, setInputDateTime] = useState(new Date()); 15 | const note = notes.find(note => note.id === noteId); 16 | const newOrEditMode = note.reminder?.dateTime ? "edit" : "new"; 17 | 18 | useEffect(() => { 19 | if (!noteReminderModal) return; 20 | if (newOrEditMode === "edit") { 21 | setInputDateTime(new Date(note.reminder?.dateTime)); 22 | } 23 | else { 24 | setInputDateTime(new Date()); 25 | } 26 | }, [noteReminderModal, note, newOrEditMode]); 27 | 28 | 29 | const onDateTimeInputChange = (e, selectedDate) => { 30 | const currentDate = selectedDate; 31 | setDateTimePickerMode(null); 32 | setInputDateTime(currentDate); 33 | }; 34 | 35 | const setOrChangeReminderForNote = async () => { 36 | try { 37 | if (newOrEditMode === "edit") { 38 | await cancelNotification(note.reminder.notifId); 39 | } 40 | const notifId = await scheduleReminderNotification({ note, dateTime: inputDateTime }); 41 | const newNotesArr = notes.map(note => { 42 | if (note.id !== noteId) return note; 43 | return { ...note, reminder: { dateTime: inputDateTime, notifId } }; 44 | }); 45 | await storeData("notes", newNotesArr); 46 | setNotes(newNotesArr); 47 | setNoteReminderModal(false); 48 | } 49 | catch (err) { 50 | Alert.alert("Error", "Some error is there!!"); 51 | } 52 | } 53 | 54 | const deleteReminderForNote = async () => { 55 | try { 56 | await cancelNotification(note.reminder.notifId); 57 | const newNotesArr = notes.map(note => { 58 | if (note.id !== noteId) return note; 59 | return { ...note, reminder: undefined }; 60 | }); 61 | await storeData("notes", newNotesArr); 62 | setNotes(newNotesArr); 63 | setNoteReminderModal(false); 64 | } 65 | catch (err) { 66 | Alert.alert("Error", "Some error is there!!"); 67 | } 68 | } 69 | 70 | 71 | return ( 72 | <> 73 | setNoteReminderModal(false)}> 74 | 75 | Set Reminder 76 | 77 | setDateTimePickerMode("date")} style={{ width: "90%", paddingVertical: 15, paddingHorizontal: 10, borderBottomWidth: 1, borderBottomColor: "#ddd" }} android_ripple={{ color: "#bbb", radius: 150 }}> 78 | {moment(inputDateTime).format("LL")} 79 | 80 | 81 | setDateTimePickerMode("time")} style={{ width: "90%", paddingVertical: 15, paddingHorizontal: 10, borderBottomWidth: 1, borderBottomColor: "#ddd" }} android_ripple={{ color: "#bbb", radius: 150 }}> 82 | {moment(inputDateTime).format("LT")} 83 | 84 | 85 | 86 | {newOrEditMode === "edit" && ( 87 | 88 | Delete 89 | 90 | )} 91 | 92 | setNoteReminderModal(false)} style={{ padding: 10 }} android_ripple={{ color: "#bbb", radius: 60 }}> 93 | Cancel 94 | 95 | 96 | 97 | Set 98 | 99 | 100 | 101 | 102 | 103 | {dateTimePickerMode && ( 104 | 109 | )} 110 | 111 | ) 112 | } 113 | 114 | export default NoteReminderModal -------------------------------------------------------------------------------- /components/Notes.js: -------------------------------------------------------------------------------- 1 | import { Alert, BackHandler, Text, ToastAndroid, View } from 'react-native' 2 | import { useIsFocused, useNavigation } from '@react-navigation/native'; 3 | import { useGlobalContext } from '../context/context'; 4 | import theme from '../style/theme'; 5 | import { useEffect } from 'react'; 6 | import React from 'react'; 7 | import NoteCard from './NoteCard'; 8 | import { getData, storeData } from '../utils/storage'; 9 | import DraggableFlatList from 'react-native-draggable-flatlist' 10 | 11 | 12 | const Notes = ({ selectedNotes, setSelectedNotes, filteredNotes, isSearchMode, setIsSearchMode }) => { 13 | const { notes, setNotes } = useGlobalContext(); 14 | const navigation = useNavigation(); 15 | const isFocussed = useIsFocused(); 16 | 17 | useEffect(() => { 18 | const backAction = () => { 19 | if (!isFocussed) return; 20 | if (selectedNotes.length > 0) { 21 | setSelectedNotes([]); 22 | return true; 23 | } 24 | if (isSearchMode) { 25 | setIsSearchMode(false); 26 | return true; 27 | } 28 | return false; 29 | }; 30 | BackHandler.addEventListener("hardwareBackPress", backAction); 31 | return () => BackHandler.removeEventListener("hardwareBackPress", backAction); 32 | }, [isFocussed, selectedNotes, setSelectedNotes, isSearchMode, setIsSearchMode]); 33 | 34 | 35 | const handlePress = (id) => { 36 | if (selectedNotes.length == 0) { 37 | navigation.navigate("UpdateNote", { id }); 38 | } 39 | else { 40 | if (selectedNotes.includes(id)) { 41 | setSelectedNotes(selectedNotes.filter(selectedNoteId => selectedNoteId !== id)); 42 | } 43 | else { 44 | setSelectedNotes([...selectedNotes, id]); 45 | } 46 | } 47 | } 48 | 49 | const handleLongPress = (id) => { 50 | if (selectedNotes.includes(id)) return; 51 | setSelectedNotes([...selectedNotes, id]); 52 | } 53 | 54 | 55 | const moveNoteToTrash = async (noteId) => { 56 | try { 57 | const note = notes.find(note => note.id === noteId); 58 | const trashNotes = (await getData("trashNotes")) || []; 59 | const newTrashNotesArr = [...trashNotes, note]; 60 | await storeData("trashNotes", newTrashNotesArr); 61 | 62 | const newNotesArr = notes.filter(note => note.id !== noteId); 63 | await storeData("notes", newNotesArr); 64 | setNotes(newNotesArr); 65 | setSelectedNotes([]); 66 | ToastAndroid.show("Note moved to trash", ToastAndroid.SHORT); 67 | } 68 | catch (err) { 69 | console.log(err); 70 | Alert.alert("Error", "Some error is there!!"); 71 | } 72 | } 73 | 74 | const changeOrderOfNotes = async ({ data }) => { 75 | try { 76 | const newNotesArr = data; 77 | if (newNotesArr.length === notes.length && JSON.stringify(newNotesArr) === JSON.stringify(notes)) return; 78 | setNotes(newNotesArr); 79 | await storeData("notes", newNotesArr); 80 | } 81 | catch (err) { 82 | console.log(err); 83 | Alert.alert("Error", "Some error is there!!"); 84 | } 85 | } 86 | 87 | 88 | const getDraggableFlatList = notes => { 89 | const notesCopy = [...notes.filter(note => note.isPinned), ...notes.filter(note => !note.isPinned)]; 90 | return note.id} 95 | renderItem={({ item: note, drag, isActive }) => ( 96 | handlePress(note.id), 99 | onLongPress: () => { handleLongPress(note.id); drag(); }, 100 | }} 101 | isAddedInSelection={selectedNotes.includes(note.id)} 102 | /> 103 | )} 104 | onDragEnd={changeOrderOfNotes} 105 | autoscrollThreshold={100} 106 | /> 107 | } 108 | 109 | 110 | return ( 111 | 112 | 113 | {notes.length === 0 && ( 114 | 115 | 116 | No note.. Tap + icon to add new note 117 | 118 | 119 | )} 120 | 121 | {notes.length > 0 && ( 122 | <> 123 | {!isSearchMode ? ( 124 | <> 125 | {notes.length} note{notes.length > 1 && 's'} 126 | 127 | {getDraggableFlatList(notes)} 128 | 129 | 130 | ) : ( 131 | <> 132 | {filteredNotes === null ? ( 133 | <> 134 | {notes.length} note{notes.length > 1 && 's'} 135 | {getDraggableFlatList(notes)} 136 | 137 | ) : filteredNotes.length === 0 ? ( 138 | No note found.. 139 | ) : ( 140 | <> 141 | {filteredNotes.length} note{filteredNotes.length > 1 && 's'} found 142 | {getDraggableFlatList(filteredNotes)} 143 | 144 | )} 145 | 146 | )} 147 | 148 | )} 149 | 150 | 151 | ) 152 | } 153 | 154 | export default Notes -------------------------------------------------------------------------------- /components/NotesPageHeader.js: -------------------------------------------------------------------------------- 1 | import { View, Text, Pressable, Alert, TextInput, ToastAndroid } from 'react-native' 2 | import React, { useEffect } from 'react' 3 | import Icon from 'react-native-vector-icons/Ionicons'; 4 | import { useGlobalContext } from '../context/context'; 5 | import { getData, storeData } from '../utils/storage'; 6 | import { getDefaultHeaderHeight } from '@react-navigation/elements'; 7 | import { useSafeAreaFrame, useSafeAreaInsets } from 'react-native-safe-area-context'; 8 | import { useNavigation } from '@react-navigation/native'; 9 | 10 | const NotesPageHeader = ({ selectedNotes, setSelectedNotes, setFilteredNotes, isSearchMode, setIsSearchMode, searchValue, setSearchValue }) => { 11 | 12 | const frame = useSafeAreaFrame(); 13 | const insets = useSafeAreaInsets(); 14 | const headerHeight = getDefaultHeaderHeight(frame, false, insets.top); 15 | const { notes, setNotes } = useGlobalContext(); 16 | const navigation = useNavigation(); 17 | 18 | // useFocusEffect(useCallback(() => { 19 | // return () => setIsSearchMode(false); 20 | // }, [setIsSearchMode])); 21 | 22 | useEffect(() => { 23 | if (searchValue === "") setFilteredNotes(null); 24 | else setFilteredNotes(notes.filter(note => note.text.toLowerCase().includes(searchValue.toLowerCase()))); 25 | }, [searchValue, notes, setFilteredNotes]); 26 | 27 | const handleDeleteMany = async () => { 28 | try { 29 | const notesToTrash = selectedNotes.map(selectedId => notes.find(note => note.id === selectedId)); 30 | const trashNotes = (await getData("trashNotes")) || []; 31 | const newTrashNotesArr = [...trashNotes, ...notesToTrash]; 32 | await storeData("trashNotes", newTrashNotesArr); 33 | 34 | const newNotesArr = notes.filter(note => !selectedNotes.includes(note.id)); 35 | await storeData("notes", newNotesArr); 36 | setNotes(newNotesArr); 37 | ToastAndroid.show("Note moved to trash", ToastAndroid.SHORT); 38 | } 39 | catch (err) { 40 | Alert.alert("Error", "Some error is there!!"); 41 | } 42 | } 43 | 44 | const handleCloseSelection = () => { 45 | setSelectedNotes([]); 46 | } 47 | 48 | const handleSearchBtnClick = () => { 49 | setIsSearchMode(true); 50 | } 51 | 52 | const handleCloseSearch = () => { 53 | setIsSearchMode(false); 54 | } 55 | 56 | const handleTextChange = (text) => { 57 | setSearchValue(text); 58 | } 59 | 60 | 61 | return ( 62 | 63 | 64 | {!isSearchMode ? ( 65 | <> 66 | {selectedNotes.length == 0 ? ( 67 | 68 | navigation.openDrawer()}> 69 | 70 | 71 | Notes 72 | 73 | 74 | 75 | 76 | ) : ( 77 | 78 | 79 | 80 | 81 | {selectedNotes.length} selected 82 | 83 | 84 | 85 | 86 | )} 87 | 88 | ) : ( 89 | 90 | 91 | 92 | 93 | 94 | setSearchValue("")} android_ripple={{ color: "#ccc", radius: 20 }}> 95 | 96 | 97 | 98 | )} 99 | 100 | 101 | ) 102 | } 103 | 104 | export default NotesPageHeader -------------------------------------------------------------------------------- /context/context.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from "react"; 2 | import { getData } from "../utils/storage"; 3 | import React from 'react' 4 | 5 | const GlobalContext = createContext(); 6 | export const GlobalContextProvider = ({ children }) => { 7 | const [notes, setNotes] = useState([]); 8 | const [labels, setLabels] = useState([]); 9 | 10 | useEffect(() => { 11 | const fetchLocalStorageData = async () => { 12 | const notes = await getData("notes"); 13 | if (notes) setNotes(notes); 14 | const labels = await getData("labels"); 15 | if (labels) setLabels(labels); 16 | } 17 | fetchLocalStorageData(); 18 | }, []); 19 | 20 | return {children} 21 | } 22 | 23 | export const useGlobalContext = () => { 24 | return useContext(GlobalContext); 25 | } -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 0.60.0" 4 | }, 5 | "build": { 6 | "development": { 7 | "developmentClient": true, 8 | "distribution": "internal" 9 | }, 10 | "preview": { 11 | "android": { 12 | "buildType": "apk" 13 | } 14 | }, 15 | "preview2": { 16 | "android": { 17 | "gradleCommand": ":app:assembleRelease" 18 | } 19 | }, 20 | "preview3": { 21 | "developmentClient": true 22 | }, 23 | "production": {} 24 | }, 25 | "submit": { 26 | "production": {} 27 | } 28 | } -------------------------------------------------------------------------------- /screens/AddNote.js: -------------------------------------------------------------------------------- 1 | import { Alert, BackHandler, TextInput, View } from 'react-native' 2 | import React, { useCallback, useEffect, useState } from 'react' 3 | import { useNavigation } from '@react-navigation/native'; 4 | import { storeData } from '../utils/storage'; 5 | import { useGlobalContext } from '../context/context'; 6 | import ActionButton from '../components/ActionButton'; 7 | 8 | const AddNote = () => { 9 | const navigation = useNavigation(); 10 | const { notes, setNotes } = useGlobalContext(); 11 | 12 | const [formData, setFormData] = useState({ 13 | text: "" 14 | }); 15 | 16 | 17 | useEffect(() => { 18 | const backAction = () => { 19 | if (formData.text === "") return false; 20 | Alert.alert("Your note has not been saved.", "Do you want to save it?", [{ text: "Yes", onPress: saveNote }, { text: "No", onPress: navigation.goBack }], { cancelable: true }); 21 | return true; 22 | } 23 | BackHandler.addEventListener("hardwareBackPress", backAction); 24 | return () => BackHandler.removeEventListener("hardwareBackPress", backAction); 25 | }, [formData, navigation, saveNote]); 26 | 27 | 28 | 29 | const handleChange = (name, value) => { 30 | setFormData({ ...formData, [name]: value }); 31 | } 32 | 33 | const saveNote = useCallback(async () => { 34 | try { 35 | if (formData.text === "") return; 36 | const newNote = { 37 | id: Math.floor(Math.random() * 10000), 38 | text: formData.text, 39 | createdAt: new Date(), 40 | updatedAt: new Date(), 41 | labels: [] 42 | } 43 | const newNotesArr = [newNote, ...notes]; 44 | await storeData("notes", newNotesArr); 45 | setNotes(newNotesArr); 46 | navigation.navigate("Home"); 47 | } 48 | catch (err) { 49 | Alert.alert("Error", "Some error is there!!"); 50 | } 51 | }, [formData, navigation, notes, setNotes]); 52 | 53 | 54 | return ( 55 | 56 | 57 | handleChange("text", text)} multiline={true} style={{ padding: 20, fontSize: 16, color: "#555" }} placeholder="Your note" autoFocus /> 58 | 59 | 60 | 61 | 62 | ) 63 | } 64 | 65 | export default AddNote -------------------------------------------------------------------------------- /screens/FolderNotes.js: -------------------------------------------------------------------------------- 1 | import { View, Text, Alert, ActivityIndicator, FlatList, Pressable, TextInput } from 'react-native' 2 | import React, { useCallback, useEffect, useRef, useState } from 'react' 3 | import { useNavigation, useRoute } from '@react-navigation/native' 4 | import { getData, storeData } from '../utils/storage'; 5 | import theme from '../style/theme'; 6 | import NoteCard from '../components/NoteCard'; 7 | import ActionButton from '../components/ActionButton'; 8 | import { useGlobalContext } from '../context/context'; 9 | import { useSafeAreaFrame, useSafeAreaInsets } from 'react-native-safe-area-context'; 10 | import { getDefaultHeaderHeight } from '@react-navigation/elements'; 11 | import Icon from 'react-native-vector-icons/Ionicons'; 12 | 13 | const FolderNotes = () => { 14 | const [folder, setFolder] = useState({}); 15 | const navigation = useNavigation(); 16 | const route = useRoute(); 17 | const { folderId } = route.params; 18 | const [loading, setLoading] = useState(true); 19 | const { notes } = useGlobalContext(); 20 | const [folderNameEditMode, setFolderNameEditMode] = useState(false); 21 | const [folderNameEditInput, setFolderNameEditInput] = useState(''); 22 | const folderNameRef = useRef(null); 23 | 24 | useEffect(() => { 25 | setFolderNameEditInput(folder.name); 26 | }, [folder]); 27 | 28 | const fetchFolderWithNotes = useCallback(async () => { 29 | try { 30 | const folders = await getData("folders"); 31 | const folder = folders.find(folder => folder.id === folderId); 32 | folder.notes = folder.notes.map(noteId => notes.find(note => note.id === noteId)); 33 | setFolder(folder); 34 | setFolderNameEditInput(folder.name); 35 | setLoading(false); 36 | } 37 | catch (err) { 38 | console.log(err); 39 | Alert.alert("Error", "Folder couldn't be fetched"); 40 | } 41 | }, [folderId, notes]); 42 | 43 | 44 | useEffect(() => { 45 | fetchFolderWithNotes(); 46 | }, [fetchFolderWithNotes]); 47 | 48 | const handlePress = (noteId) => { 49 | navigation.navigate("UpdateNote", { id: noteId }); 50 | } 51 | 52 | 53 | const getFlatList = folderNotes => { 54 | return note.id} 59 | renderItem={({ item: note }) => handlePress(note.id) }} />} 60 | /> 61 | } 62 | 63 | const onNotesSelected = async selectedNotes => { 64 | try { 65 | setLoading(true); 66 | const folders = await getData("folders"); 67 | const newFoldersArr = folders.map(folder => { 68 | if (folder.id !== folderId) return folder; 69 | return { ...folder, notes: selectedNotes }; 70 | }); 71 | await storeData("folders", newFoldersArr); 72 | fetchFolderWithNotes(); 73 | setLoading(false); 74 | } 75 | catch (err) { 76 | console.log(err); 77 | Alert.alert("Error", "Notes couldn't be added into folder"); 78 | } 79 | } 80 | 81 | 82 | const frame = useSafeAreaFrame(); 83 | const insets = useSafeAreaInsets(); 84 | const headerHeight = getDefaultHeaderHeight(frame, false, insets.top); 85 | 86 | const handleFolderNameFocus = () => { 87 | setFolderNameEditMode(true); 88 | } 89 | 90 | const handleBackClick = () => { 91 | if (folderNameEditMode) { 92 | setFolderNameEditInput(folder.name); 93 | setFolderNameEditMode(false); 94 | } 95 | else { 96 | navigation.goBack(); 97 | } 98 | } 99 | 100 | const changeFolderName = async () => { 101 | try { 102 | if (folderNameEditInput === folder.name) return; 103 | const folders = await getData("folders"); 104 | const newFoldersArr = folders.map(folder => { 105 | if (folder.id !== folderId) return folder; 106 | return { ...folder, name: folderNameEditInput }; 107 | }); 108 | await storeData("folders", newFoldersArr); 109 | setFolderNameEditMode(false); 110 | setFolder({ ...folder, name: folderNameEditInput }); 111 | } 112 | catch (err) { 113 | console.log(err); 114 | Alert.alert("Error", "Couldn't change folder name"); 115 | } 116 | } 117 | 118 | useEffect(() => { 119 | if (folderNameEditMode === false) folderNameRef?.current?.blur(); 120 | }, [folderNameEditMode]); 121 | 122 | 123 | if (loading) { 124 | return ( 125 | 126 | 127 | 128 | ); 129 | } 130 | 131 | return ( 132 | 133 | 134 | {/* Header */} 135 | 136 | 137 | 138 | 139 | 140 | setFolderNameEditInput(value)} 146 | onFocus={handleFolderNameFocus} 147 | onSubmitEditing={changeFolderName} 148 | /> 149 | {folderNameEditMode && ( 150 | 151 | 152 | 153 | )} 154 | 155 | 156 | 157 | {/* Body */} 158 | 159 | 160 | {folder.notes.length === 0 && ( 161 | 162 | 163 | No notes in this folder.. {"\n\n"} Tap + icon to add notes in this folder 164 | 165 | 166 | )} 167 | 168 | {folder.notes.length > 0 && ( 169 | <> 170 | {folder.notes.length} note{folder.notes.length > 1 && 's'} 171 | {getFlatList(folder.notes)} 172 | 173 | )} 174 | 175 | navigation.navigate("NotesSelector", { onNotesSelected, unSelectableNotes: folder.notes.map(note => note.id) })} /> 176 | 177 | 178 | ) 179 | } 180 | 181 | export default FolderNotes -------------------------------------------------------------------------------- /screens/Folders.js: -------------------------------------------------------------------------------- 1 | import { View, Text, TextInput, Pressable, Alert, ToastAndroid, ActivityIndicator } from 'react-native' 2 | import React, { useCallback, useState } from 'react' 3 | import ActionButton from '../components/ActionButton'; 4 | import Modal from '../components/Modal'; 5 | import { getData, storeData } from '../utils/storage'; 6 | import DraggableFlatList from 'react-native-draggable-flatlist'; 7 | import theme from '../style/theme'; 8 | import FolderCard from '../components/FolderCard'; 9 | import { useFocusEffect, useNavigation } from '@react-navigation/native'; 10 | 11 | const Folders = () => { 12 | const [folders, setFolders] = useState([]); 13 | const [createFolderModal, setCreateFolderModal] = useState(false); 14 | const [FolderNameInput, setFolderNameInput] = useState(""); 15 | const navigation = useNavigation(); 16 | const [loading, setLoading] = useState(true); 17 | 18 | useFocusEffect(useCallback(() => { 19 | const fetchFolders = async () => { 20 | try { 21 | const folders = (await getData("folders")) || []; 22 | setFolders(folders); 23 | setLoading(false); 24 | } 25 | catch (err) { 26 | console.log(err); 27 | Alert.alert("Error", "Folders couldn't be fetched"); 28 | } 29 | } 30 | fetchFolders(); 31 | }, [])); 32 | 33 | const closeModal = () => { 34 | setCreateFolderModal(false); 35 | setFolderNameInput(""); 36 | } 37 | 38 | const createFolder = async () => { 39 | try { 40 | if (FolderNameInput === "") return; 41 | const newFolder = { 42 | id: Math.floor(Math.random() * 10000), 43 | name: FolderNameInput, 44 | notes: [], 45 | createdAt: new Date(), 46 | }; 47 | 48 | const newFoldersArr = [newFolder, ...folders]; 49 | await storeData("folders", newFoldersArr); 50 | setFolders(newFoldersArr); 51 | ToastAndroid.show("Folder created", ToastAndroid.SHORT); 52 | closeModal(); 53 | } 54 | catch (err) { 55 | console.log(err); 56 | Alert.alert("Error", "Some error is there!!"); 57 | } 58 | } 59 | 60 | const changeOrderOfFolders = async ({ data }) => { 61 | try { 62 | const newFoldersArr = data; 63 | if (newFoldersArr.length === folders.length && JSON.stringify(newFoldersArr.map(folder => folder.id)) === JSON.stringify(folders.map(folder => folder.id))) return; 64 | setFolders(newFoldersArr); 65 | await storeData("folders", newFoldersArr); 66 | } 67 | catch (err) { 68 | console.log(err); 69 | Alert.alert("Error", "Some error is there!!"); 70 | } 71 | } 72 | 73 | const handlePress = folderId => { 74 | navigation.navigate("FolderNotes", { folderId }); 75 | } 76 | 77 | const getDraggableFlatList = folders => { 78 | return folder.id} 84 | renderItem={({ item: folder, drag, isActive }) => } 85 | onDragEnd={changeOrderOfFolders} 86 | autoscrollThreshold={100} 87 | /> 88 | } 89 | 90 | 91 | 92 | if (loading) { 93 | return ( 94 | 95 | 96 | 97 | ); 98 | } 99 | 100 | return ( 101 | 102 | 103 | {folders.length === 0 && ( 104 | 105 | 106 | No folders present.. Tap + icon to create new folder 107 | 108 | 109 | )} 110 | 111 | {folders.length > 0 && ( 112 | <> 113 | {folders.length} folder{folders.length > 1 && 's'} 114 | {getDraggableFlatList(folders)} 115 | 116 | )} 117 | 118 | 119 | 120 | setCreateFolderModal(true)} /> 121 | 122 | 123 | New folder 124 | setFolderNameInput(text)} 127 | placeholder='Folder name' 128 | style={{ backgroundColor: "#eee", paddingHorizontal: 15, paddingVertical: 5, borderRadius: 2 }} 129 | /> 130 | 131 | 132 | 138 | Create 139 | 140 | 141 | 146 | Cancel 147 | 148 | 149 | 150 | 151 | 152 | ) 153 | } 154 | 155 | export default Folders -------------------------------------------------------------------------------- /screens/Home.js: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native' 2 | import React, { useEffect, useState } from 'react' 3 | import Notes from '../components/Notes' 4 | import ActionButton from '../components/ActionButton' 5 | import NotesPageHeader from '../components/NotesPageHeader' 6 | import { useNavigation } from '@react-navigation/native' 7 | 8 | const Home = () => { 9 | 10 | const [selectedNotes, setSelectedNotes] = useState([]); 11 | const [isSearchMode, setIsSearchMode] = useState(false); 12 | const [searchValue, setSearchValue] = useState(""); 13 | const [filteredNotes, setFilteredNotes] = useState(null); 14 | const navigation = useNavigation(); 15 | useEffect(() => { 16 | if (!isSearchMode) setSearchValue(""); 17 | }, [isSearchMode]); 18 | 19 | return ( 20 | <> 21 | 22 | 23 | 24 | navigation.navigate("AddNote")} /> 25 | 26 | 27 | ) 28 | } 29 | 30 | export default Home -------------------------------------------------------------------------------- /screens/LabelsManager.js: -------------------------------------------------------------------------------- 1 | import { View, Text, TextInput, Pressable, ToastAndroid, Alert, ScrollView } from 'react-native' 2 | import React, { useEffect, useState } from 'react' 3 | import { useGlobalContext } from '../context/context'; 4 | import theme from '../style/theme'; 5 | import { storeData } from '../utils/storage'; 6 | import Modal from '../components/Modal'; 7 | 8 | const LabelsManager = () => { 9 | const [labelSearchInput, setlabelSearchInput] = useState(""); 10 | const { notes, setNotes, labels, setLabels } = useGlobalContext(); 11 | 12 | const [modalLabel, setModalLabel] = useState(null); 13 | const [editLabelInput, setEditLabelInput] = useState(""); 14 | 15 | useEffect(() => { 16 | if (modalLabel) setEditLabelInput(modalLabel); 17 | }, [modalLabel]); 18 | 19 | const filteredLabels = labelSearchInput === "" ? labels : labels.filter(label => label.includes(labelSearchInput)); 20 | 21 | const createLabel = async () => { 22 | try { 23 | if (labelSearchInput === "") return; 24 | const newLabelsArr = [...labels, labelSearchInput]; 25 | await storeData("labels", newLabelsArr); 26 | setLabels(newLabelsArr); 27 | ToastAndroid.show("Label added", ToastAndroid.SHORT); 28 | setlabelSearchInput(""); 29 | } 30 | catch (err) { 31 | console.log(err); 32 | Alert.alert("Error", "Some error is there!!"); 33 | } 34 | } 35 | 36 | const saveLabel = async () => { 37 | try { 38 | if (!editLabelInput) return; 39 | if (modalLabel === editLabelInput) { 40 | setModalLabel(null); 41 | return; 42 | } 43 | 44 | const newLabelsArr = labels.map(label => { 45 | if (label !== modalLabel) return label; 46 | return editLabelInput; 47 | }); 48 | await storeData("labels", newLabelsArr); 49 | setLabels(newLabelsArr); 50 | 51 | const newNotesArr = notes.map(note => { 52 | if (!note.labels) return note; 53 | note.labels = note.labels.map(label => { 54 | if (label !== modalLabel) return label; 55 | return editLabelInput; 56 | }); 57 | return note; 58 | }); 59 | await storeData("notes", newNotesArr); 60 | setNotes(newNotesArr); 61 | 62 | ToastAndroid.show("Label changed", ToastAndroid.SHORT); 63 | setModalLabel(null); 64 | } 65 | catch (err) { 66 | console.log(err); 67 | Alert.alert("Error", "Some error is there!!"); 68 | } 69 | } 70 | 71 | const deleteLabel = async () => { 72 | try { 73 | const newLabelsArr = labels.filter(label => label !== modalLabel); 74 | await storeData("labels", newLabelsArr); 75 | setLabels(newLabelsArr); 76 | 77 | const newNotesArr = notes.map(note => { 78 | if (!note.labels) return note; 79 | note.labels = note.labels.filter(label => label !== modalLabel); 80 | return note; 81 | }); 82 | await storeData("notes", newNotesArr); 83 | setNotes(newNotesArr); 84 | 85 | ToastAndroid.show("Label deleted from all such notes", ToastAndroid.SHORT); 86 | setModalLabel(null); 87 | } 88 | catch (err) { 89 | console.log(err); 90 | Alert.alert("Error", "Some error is there!!"); 91 | } 92 | } 93 | 94 | return ( 95 | 96 | setlabelSearchInput(text)} 99 | placeholder='Search or create label...' 100 | style={{ color: "#666", fontWeight: "500", fontSize: 17, backgroundColor: "white", paddingVertical: 10, paddingHorizontal: 15, borderBottomWidth: 1, borderBottomColor: '#ddd', borderTopColor: "#ddd", borderTopWidth: 1 }} 101 | /> 102 | 103 | {labels.length} total 104 | 105 | {labelSearchInput !== "" && !labels.includes(labelSearchInput) && ( 106 | 107 | + Create label `{labelSearchInput}` 108 | 109 | )} 110 | 111 | 112 | {filteredLabels.map(label => ( 113 | setModalLabel(label)} 115 | style={{ margin: 10, paddingVertical: 5, paddingHorizontal: 10, borderRadius: 3, backgroundColor: "#0193fe" }} 116 | android_ripple={{ color: theme.PRIMARY_COLOR, radius: 200 }} 117 | > 118 | {label} 119 | 120 | ))} 121 | 122 | 123 | 124 | 125 | setModalLabel(null)}> 126 | 127 | 128 | setEditLabelInput(text)} /> 133 | 134 | 135 | 141 | Save 142 | 143 | 144 | 149 | Delete 150 | 151 | 152 | 153 | 154 | 155 | ) 156 | } 157 | 158 | export default LabelsManager 159 | -------------------------------------------------------------------------------- /screens/NoteLabelsManager.js: -------------------------------------------------------------------------------- 1 | import { View, Text, TextInput, Pressable, Alert, ToastAndroid, BackHandler, ScrollView } from 'react-native' 2 | import React, { useEffect, useState } from 'react' 3 | import theme from '../style/theme'; 4 | import { useGlobalContext } from '../context/context'; 5 | import { storeData } from '../utils/storage'; 6 | import { useNavigation, useRoute } from '@react-navigation/native'; 7 | import ActionButton from '../components/ActionButton'; 8 | 9 | const NoteLabelsManager = () => { 10 | 11 | const navigation = useNavigation(); 12 | const route = useRoute(); 13 | const { notes, setNotes, labels, setLabels } = useGlobalContext(); 14 | const [labelInput, setlabelInput] = useState(""); 15 | 16 | const noteId = route.params.noteId; 17 | const note = notes.find(note => note.id === noteId); 18 | const [tempMarkedLabels, setTempMarkedLabels] = useState(note.labels); 19 | 20 | useEffect(() => { 21 | setTempMarkedLabels(note.labels); 22 | }, [note.labels]); 23 | 24 | const filteredLabels = labelInput === "" ? labels : labels.filter(label => label.includes(labelInput)); 25 | const reorderedLabels = [...filteredLabels.filter(label => note.labels.includes(label)), ...filteredLabels.filter(label => !note.labels.includes(label))]; 26 | 27 | 28 | const areLabelsModified = (() => { 29 | if (tempMarkedLabels.length !== note.labels.length) return true; 30 | return tempMarkedLabels.some(tempMarkedLabel => !note.labels.includes(tempMarkedLabel)); 31 | })(); 32 | 33 | 34 | useEffect(() => { 35 | const backAction = () => { 36 | if (!areLabelsModified) return false; 37 | Alert.alert("Discard changes", "", [{ text: "Cancel" }, { text: "Discard", onPress: navigation.goBack }], { cancelable: true }); 38 | return true; 39 | } 40 | BackHandler.addEventListener("hardwareBackPress", backAction); 41 | return () => BackHandler.removeEventListener("hardwareBackPress", backAction); 42 | }, [areLabelsModified, navigation]); 43 | 44 | 45 | 46 | const handleLabelInputChange = (text) => { 47 | setlabelInput(text); 48 | } 49 | 50 | 51 | const createAndAddLabel = async () => { 52 | try { 53 | if (labelInput === "") return; 54 | const newLabelsArr = [...labels, labelInput]; 55 | await storeData("labels", newLabelsArr); 56 | setLabels(newLabelsArr); 57 | 58 | const newNotesArr = notes.map(note => { 59 | if (note.id !== noteId) return note; 60 | return { ...note, labels: [...(note.labels || []), labelInput] }; 61 | }); 62 | await storeData("notes", newNotesArr); 63 | setNotes(newNotesArr); 64 | ToastAndroid.show("Label added", ToastAndroid.SHORT); 65 | setlabelInput(""); 66 | } 67 | catch (err) { 68 | console.log(err); 69 | Alert.alert("Error", "Some error is there!!"); 70 | } 71 | } 72 | 73 | 74 | const changeLabelsOfNote = async () => { 75 | try { 76 | const newNotesArr = notes.map(note => { 77 | if (note.id !== noteId) return note; 78 | return { ...note, labels: tempMarkedLabels }; 79 | }); 80 | await storeData("notes", newNotesArr); 81 | setNotes(newNotesArr); 82 | setlabelInput(""); 83 | navigation.goBack(); 84 | } 85 | catch (err) { 86 | console.log(err); 87 | Alert.alert("Error", "Some error is there!!"); 88 | } 89 | } 90 | 91 | 92 | const toggleTempLabelForNote = label => { 93 | if (tempMarkedLabels.includes(label)) setTempMarkedLabels(tempMarkedLabels.filter(tempLabel => tempLabel !== label)); 94 | else setTempMarkedLabels([...tempMarkedLabels, label]); 95 | } 96 | 97 | 98 | return ( 99 | 100 | 101 | 102 | {labels.length} total, {tempMarkedLabels.length} selected 103 | 104 | {labelInput !== "" && !labels.includes(labelInput) && ( 105 | 106 | + Create and add label `{labelInput}` 107 | 108 | )} 109 | 110 | 111 | {reorderedLabels.map(label => ( 112 | toggleTempLabelForNote(label)} 114 | style={{ margin: 10, paddingVertical: 5, paddingHorizontal: 10, borderRadius: 3, backgroundColor: tempMarkedLabels.includes(label) ? "#0193fe" : "#dff6ff" }} 115 | android_ripple={{ color: theme.PRIMARY_COLOR, radius: 200 }} 116 | > 117 | 118 | {label} 119 | 120 | 121 | ))} 122 | 123 | 124 | 125 | 126 | ) 127 | } 128 | 129 | export default NoteLabelsManager 130 | -------------------------------------------------------------------------------- /screens/NotesSelector.js: -------------------------------------------------------------------------------- 1 | import { View, Text, FlatList, Pressable } from 'react-native' 2 | import React, { useState } from 'react' 3 | import ActionButton from '../components/ActionButton' 4 | import { useGlobalContext } from '../context/context'; 5 | import NoteCardText from '../components/NoteCardText'; 6 | import { useNavigation, useRoute } from '@react-navigation/native'; 7 | import theme from '../style/theme'; 8 | import Icon from 'react-native-vector-icons/Ionicons'; 9 | 10 | 11 | const NoteSelectorCard = ({ note, onPress, isAddedInSelection }) => { 12 | return ( 13 | 22 | {isAddedInSelection && ( 23 | 24 | 25 | 26 | )} 27 | 28 | 29 | ); 30 | } 31 | 32 | 33 | const NotesSelector = () => { 34 | const [selectedNotes, setSelectedNotes] = useState([]); 35 | const { notes } = useGlobalContext(); 36 | const navigation = useNavigation(); 37 | const route = useRoute(); 38 | const { unSelectableNotes = [], onNotesSelected } = route.params; 39 | const selectableNotes = notes.filter(note => !unSelectableNotes.includes(note.id)); 40 | 41 | const handlePress = (id) => { 42 | if (selectedNotes.includes(id)) { 43 | setSelectedNotes(selectedNotes.filter(selectedNoteId => selectedNoteId !== id)); 44 | } 45 | else { 46 | setSelectedNotes([...selectedNotes, id]); 47 | } 48 | } 49 | 50 | const getFlatList = notes => { 51 | return note.id} 56 | renderItem={({ item: note }) => ( 57 | handlePress(note.id)} 60 | isAddedInSelection={selectedNotes.includes(note.id)} 61 | /> 62 | )} 63 | /> 64 | } 65 | 66 | const onDone = () => { 67 | onNotesSelected(selectedNotes); 68 | navigation.goBack(); 69 | } 70 | 71 | 72 | 73 | return ( 74 | 75 | {selectedNotes.length} selected 76 | {getFlatList(selectableNotes)} 77 | 78 | 0} /> 79 | 80 | ) 81 | } 82 | 83 | export default NotesSelector -------------------------------------------------------------------------------- /screens/Trash.js: -------------------------------------------------------------------------------- 1 | import { View, Text, Alert, ToastAndroid, ScrollView, StyleSheet, Pressable } from 'react-native' 2 | import React, { useCallback, useState } from 'react' 3 | import { getData, storeData } from '../utils/storage'; 4 | import { useFocusEffect } from '@react-navigation/native'; 5 | import theme from '../style/theme'; 6 | import { FlatList } from 'react-native-gesture-handler'; 7 | import Icon from 'react-native-vector-icons/Ionicons'; 8 | import { convertToXTimeAgo } from '../utils/dateformat'; 9 | import { useGlobalContext } from '../context/context'; 10 | import Modal from '../components/Modal'; 11 | 12 | 13 | const TrashNoteCard = ({ note, setNoteModalId }) => { 14 | return ( 15 | setNoteModalId(note.id)} 17 | android_ripple={{ color: "#bbb", radius: 200 }} 18 | style={styles.noteCard} 19 | delayLongPress={100} 20 | > 21 | 22 | 23 | {note.color && ( 24 | 25 | )} 26 | {convertToXTimeAgo(note.updatedAt)} 27 | {note.isBookmarked && ( 28 | 29 | )} 30 | {note.isPinned && ( 31 | 32 | )} 33 | 34 | 35 | {note.labels.length > 0 && ( 36 | 37 | {note.labels?.map(label => ( 38 | {label} 39 | ))} 40 | 41 | )} 42 | 43 | {note.reminder?.dateTime && ( 44 | + Reminder 45 | )} 46 | 47 | {note.text} 48 | 49 | 50 | ); 51 | } 52 | 53 | 54 | const Trash = () => { 55 | const [trashNotes, setTrashNotes] = useState([]); 56 | const { notes, setNotes } = useGlobalContext(); 57 | const [noteModalId, setNoteModalId] = useState(null); 58 | 59 | useFocusEffect(useCallback(() => { 60 | const fetchTrash = async () => { 61 | try { 62 | const trashNotes = (await getData("trashNotes")) || []; 63 | setTrashNotes(trashNotes); 64 | } 65 | catch (err) { 66 | Alert.alert("Error", "Some error is there!!"); 67 | } 68 | } 69 | fetchTrash(); 70 | }, [])); 71 | 72 | const deleteNotePermanently = async noteId => { 73 | try { 74 | const newTrashNotesArr = trashNotes.filter(trashNote => trashNote.id !== noteId); 75 | await storeData("trashNotes", newTrashNotesArr); 76 | setTrashNotes(newTrashNotesArr); 77 | setNoteModalId(null); 78 | ToastAndroid.show("Note permanently deleted", ToastAndroid.SHORT); 79 | } 80 | catch (err) { 81 | Alert.alert("Error", "Some error is there!!"); 82 | } 83 | } 84 | 85 | const emptyTrash = async () => { 86 | try { 87 | await storeData("trashNotes", []); 88 | setTrashNotes([]); 89 | ToastAndroid.show("Trash is empty now", ToastAndroid.SHORT); 90 | } 91 | catch (err) { 92 | Alert.alert("Error", "Some error is there!!"); 93 | } 94 | } 95 | 96 | const handleEmptyTrashRequest = () => { 97 | Alert.alert( 98 | "Are you sure you want to empty the trash?", "You won't be able to restore these notes later.", 99 | [{ text: "Cancel" }, { text: "OK", onPress: emptyTrash }], 100 | { cancelable: true } 101 | ); 102 | } 103 | 104 | 105 | const restoreNote = async noteId => { 106 | try { 107 | const note = trashNotes.find(note => note.id === noteId); 108 | const newNotesArr = [...notes, note]; 109 | await storeData("notes", newNotesArr); 110 | setNotes(newNotesArr); 111 | 112 | const newTrashNotesArr = trashNotes.filter(trashNote => trashNote.id !== noteId); 113 | await storeData("trashNotes", newTrashNotesArr); 114 | setTrashNotes(newTrashNotesArr); 115 | setNoteModalId(null); 116 | ToastAndroid.show("Note restored", ToastAndroid.SHORT); 117 | } 118 | catch (err) { 119 | Alert.alert("Error", "Some error is there!!"); 120 | } 121 | } 122 | 123 | const restoreAllNotes = async () => { 124 | try { 125 | const newNotesArr = [...notes, ...trashNotes]; 126 | await storeData("notes", newNotesArr); 127 | setNotes(newNotesArr); 128 | await storeData("trashNotes", []); 129 | setTrashNotes([]); 130 | ToastAndroid.show("All notes restored", ToastAndroid.SHORT); 131 | } 132 | catch (err) { 133 | Alert.alert("Error", "Some error is there!!"); 134 | } 135 | } 136 | 137 | const handleRestoreRequest = () => { 138 | Alert.alert( 139 | "Restore all notes", null, 140 | [{ text: "Cancel" }, { text: "OK", onPress: restoreAllNotes }], 141 | { cancelable: true } 142 | ); 143 | } 144 | 145 | 146 | return ( 147 | 148 | {trashNotes.length === 0 && ( 149 | 150 | 151 | Trash is empty 152 | 153 | 154 | )} 155 | 156 | {trashNotes.length > 0 && ( 157 | <> 158 | 159 | {trashNotes.length} note{trashNotes.length > 1 && 's'} in trash 160 | 165 | Restore 166 | 167 | 172 | Empty 173 | 174 | 175 | 176 | note.id} 180 | renderItem={({ item: note }) => } 181 | /> 182 | 183 | 184 | )} 185 | 186 | setNoteModalId(null)}> 187 | 188 | restoreNote(noteModalId)} 190 | style={{ paddingVertical: 10 }} 191 | android_ripple={{ color: "#bbb", radius: 200 }}> 192 | Restore 193 | 194 | 195 | deleteNotePermanently(noteModalId)} 197 | style={{ paddingVertical: 10 }} 198 | android_ripple={{ color: "#bbb", radius: 200 }}> 199 | Delete permanently 200 | 201 | 202 | 203 | 204 | ) 205 | } 206 | 207 | export default Trash 208 | 209 | const styles = StyleSheet.create({ 210 | noteCard: { 211 | marginBottom: 20, 212 | marginHorizontal: 10, 213 | borderRadius: 3, 214 | paddingHorizontal: 15, 215 | paddingTop: 10, 216 | paddingBottom: 15, 217 | backgroundColor: "white", 218 | borderBottomWidth: 2, 219 | borderBottomColor: "#e4e4e4", 220 | } 221 | }) -------------------------------------------------------------------------------- /screens/UpdateNote.js: -------------------------------------------------------------------------------- 1 | import { Alert, Pressable, Text, TextInput, View, ToastAndroid, BackHandler } from 'react-native' 2 | import React, { useCallback, useEffect, useState } from 'react' 3 | import { useIsFocused, useNavigation, useRoute } from '@react-navigation/native'; 4 | import { storeData } from '../utils/storage'; 5 | import { useGlobalContext } from '../context/context'; 6 | import Icon from 'react-native-vector-icons/Ionicons'; 7 | import { convertToXTimeAgo } from '../utils/dateformat'; 8 | import { SheetManager } from "react-native-actions-sheet"; 9 | import NoteOptionsActionSheet from '../components/NoteOptionsActionSheet'; 10 | import moment from 'moment'; 11 | import NoteReminderModal from '../components/NoteReminderModal'; 12 | import ActionButton from '../components/ActionButton'; 13 | import { SharedElement } from 'react-navigation-shared-element'; 14 | 15 | const UpdateNote = () => { 16 | const navigation = useNavigation(); 17 | const route = useRoute(); 18 | const isFocussed = useIsFocused(); 19 | const { notes, setNotes } = useGlobalContext(); 20 | const [noteReminderModal, setNoteReminderModal] = useState(false); 21 | 22 | const noteId = route.params.id; 23 | const note = notes.find(note => note.id === noteId); 24 | const [formData, setFormData] = useState({ text: note.text }); 25 | 26 | useEffect(() => { 27 | const backAction = () => { 28 | if (!isFocussed) return false; 29 | if (formData.text === "" || formData.text === note?.text) return false; 30 | Alert.alert("Your changes have not been saved.", "Do you want to save it?", [{ text: "Yes", onPress: updateNote }, { text: "No", onPress: navigation.goBack }], { cancelable: true }); 31 | return true; 32 | } 33 | BackHandler.addEventListener("hardwareBackPress", backAction); 34 | return () => BackHandler.removeEventListener("hardwareBackPress", backAction); 35 | }, [isFocussed, formData, navigation, updateNote, note?.text]); 36 | 37 | 38 | const handleChange = (name, value) => { 39 | setFormData({ ...formData, [name]: value }); 40 | } 41 | 42 | const updateNote = useCallback(async () => { 43 | try { 44 | if (formData.text === "") return; 45 | const newNotesArr = notes.map(note => { 46 | if (note.id !== noteId) return note; 47 | return { ...note, text: formData.text, updatedAt: new Date() }; 48 | }) 49 | await storeData("notes", newNotesArr); 50 | setNotes(newNotesArr); 51 | navigation.navigate("Home"); 52 | ToastAndroid.show("Note saved", ToastAndroid.SHORT); 53 | } 54 | catch (err) { 55 | Alert.alert("Error", "Some error is there!!"); 56 | } 57 | }, [formData, notes, setNotes, navigation, noteId]); 58 | 59 | 60 | const toggleBookmark = async () => { 61 | try { 62 | const newNotesArr = notes.map(note => { 63 | if (note.id !== noteId) return note; 64 | return { ...note, isBookmarked: !note.isBookmarked }; 65 | }) 66 | await storeData("notes", newNotesArr); 67 | setNotes(newNotesArr); 68 | } 69 | catch (err) { 70 | Alert.alert("Error", "Some error is there!!"); 71 | } 72 | } 73 | 74 | const updatedAt = convertToXTimeAgo(note.updatedAt); 75 | const isReminderTimePassed = new Date(note.reminder?.dateTime).getTime() < new Date().getTime(); 76 | 77 | 78 | return ( 79 | 80 | 81 | 82 | 83 | {note.labels?.map(label => ( 84 | {label} 85 | ))} 86 | 87 | 88 | {note.reminder?.dateTime && ( 89 | setNoteReminderModal(true)} style={{ flexDirection: "row", alignSelf: "flex-start", margin: 10, marginTop: 0, paddingVertical: 5, paddingHorizontal: 10, backgroundColor: "#eee", borderRadius: 3, }} android_ripple={{ color: "#bbb", radius: 200 }}> 90 | 91 | 92 | {moment(note.reminder.dateTime).format("lll")} 93 | 94 | 95 | )} 96 | 97 | handleChange("text", text)} 100 | multiline={true} 101 | style={{ paddingHorizontal: 20, paddingBottom: 100, fontSize: 16, color: "#555" }} 102 | placeholder="Your note" 103 | /> 104 | 105 | 106 | 107 | 108 | 109 | Edited {updatedAt} 110 | 111 | 112 | 113 | SheetManager.show("noteOptionsActionSheet")} android_ripple={{ color: "#ccc", radius: 30 }}> 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | {note.reminder?.dateTime && ( 122 | 123 | )} 124 | 125 | ) 126 | } 127 | 128 | 129 | UpdateNote.sharedElements = route => { 130 | return [ 131 | { 132 | id: `note.${route.params.id}`, 133 | animation: "fade" 134 | } 135 | ]; 136 | } 137 | 138 | export default UpdateNote -------------------------------------------------------------------------------- /screens/index.js: -------------------------------------------------------------------------------- 1 | export { default as AddNote } from "./AddNote" 2 | export { default as Folders } from "./Folders" 3 | export { default as FolderNotes } from "./FolderNotes" 4 | export { default as Home } from "./Home" 5 | export { default as LabelsManager } from "./LabelsManager" 6 | export { default as NoteLabelsManager } from "./NoteLabelsManager" 7 | export { default as NotesSelector } from "./NotesSelector" 8 | export { default as Trash } from "./Trash" 9 | export { default as UpdateNote } from "./UpdateNote" -------------------------------------------------------------------------------- /style/theme.js: -------------------------------------------------------------------------------- 1 | export default { 2 | PRIMARY_COLOR: '#1d9bff' 3 | }; -------------------------------------------------------------------------------- /utils/dateformat.js: -------------------------------------------------------------------------------- 1 | const convertToFullDate = (dateString) => { 2 | return new Date(dateString).toLocaleString("en-IN", { day: "numeric", month: "short", year: "numeric", hour: "numeric", minute: "numeric" }); 3 | } 4 | 5 | const convertToXTimeAgo = (dateString) => { 6 | const seconds = Math.floor((new Date() - new Date(dateString)) / 1000); 7 | const mins = Math.floor(seconds / 60); 8 | const hours = Math.floor(mins / 60); 9 | const days = Math.floor(hours / 24); 10 | const months = Math.floor(days / 30); 11 | const years = Math.floor(days / 365); 12 | 13 | if (years > 0) { 14 | return `${years} ${years === 1 ? "yr" : "yrs"} ago`; 15 | } 16 | if (months > 0) { 17 | return `${months} ${months === 1 ? "month" : "months"} ago`; 18 | } 19 | if (days > 0) { 20 | return `${days} ${days === 1 ? "day" : "days"} ago`; 21 | } 22 | if (hours > 0) { 23 | return `${hours} ${hours === 1 ? "hr" : "hrs"} ago`; 24 | } 25 | if (mins > 0) { 26 | return `${mins} ${mins === 1 ? "min" : "mins"} ago`; 27 | } 28 | return "just now"; 29 | } 30 | 31 | export { convertToFullDate as convertToAbsoluteDate, convertToXTimeAgo } -------------------------------------------------------------------------------- /utils/storage.js: -------------------------------------------------------------------------------- 1 | import AsyncStorage from '@react-native-async-storage/async-storage'; 2 | 3 | const storeData = async (key, value) => { 4 | try { 5 | const jsonValue = JSON.stringify(value); 6 | await AsyncStorage.setItem(key, jsonValue); 7 | return Promise.resolve(); 8 | } catch (e) { 9 | return Promise.reject(e); 10 | } 11 | } 12 | 13 | const getData = async (key) => { 14 | try { 15 | const jsonValue = await AsyncStorage.getItem(key) 16 | return jsonValue != null ? JSON.parse(jsonValue) : null; 17 | } catch (e) { 18 | return Promise.reject(e); 19 | } 20 | } 21 | 22 | export { storeData, getData }; --------------------------------------------------------------------------------