diff --git a/index.js b/index.js index 9096a00..0116efe 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ import { AppRegistry } from 'react-native'; import App from './src/app'; import { name as appName } from './app.json'; +import 'react-native-get-random-values'; AppRegistry.registerComponent(appName, () => App); diff --git a/package-lock.json b/package-lock.json index d109f3b..f596e13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "react-native-fast-image": "^8.6.3", "react-native-file-access": "^3.0.4", "react-native-gesture-handler": "^2.12.0", + "react-native-get-random-values": "^1.9.0", "react-native-paper": "^5.9.1", "react-native-reanimated": "^3.3.0", "react-native-safe-area-context": "^4.6.4", @@ -68,7 +69,7 @@ "typescript": "^4.8.4" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -4572,6 +4573,16 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "18.2.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", + "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", + "optional": true, + "peer": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-native": { "version": "0.70.14", "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.14.tgz", @@ -7414,6 +7425,11 @@ "node >=0.6.0" ] }, + "node_modules/fast-base64-decode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz", + "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -13389,6 +13405,17 @@ "react-native": "*" } }, + "node_modules/react-native-get-random-values": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.9.0.tgz", + "integrity": "sha512-+29IR2oxzxNVeaRwCqGZ9ABadzMI8SLTBidrIDXPOkKnm5+kEmLt34QKM4JV+d2usPErvKyS85le0OmGTHnyWQ==", + "dependencies": { + "fast-base64-decode": "^1.0.0" + }, + "peerDependencies": { + "react-native": ">=0.56" + } + }, "node_modules/react-native-paper": { "version": "5.9.1", "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.9.1.tgz", @@ -19111,6 +19138,16 @@ "csstype": "^3.0.2" } }, + "@types/react-dom": { + "version": "18.2.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", + "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", + "optional": true, + "peer": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-native": { "version": "0.70.14", "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.14.tgz", @@ -21192,6 +21229,11 @@ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" }, + "fast-base64-decode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz", + "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==" + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -25734,6 +25776,14 @@ "prop-types": "^15.7.2" } }, + "react-native-get-random-values": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.9.0.tgz", + "integrity": "sha512-+29IR2oxzxNVeaRwCqGZ9ABadzMI8SLTBidrIDXPOkKnm5+kEmLt34QKM4JV+d2usPErvKyS85le0OmGTHnyWQ==", + "requires": { + "fast-base64-decode": "^1.0.0" + } + }, "react-native-paper": { "version": "5.9.1", "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.9.1.tgz", diff --git a/package.json b/package.json index 25b3d54..e09d771 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "react-native-fast-image": "^8.6.3", "react-native-file-access": "^3.0.4", "react-native-gesture-handler": "^2.12.0", + "react-native-get-random-values": "^1.9.0", "react-native-paper": "^5.9.1", "react-native-reanimated": "^3.3.0", "react-native-safe-area-context": "^4.6.4", @@ -73,6 +74,6 @@ "typescript": "^4.8.4" }, "engines": { - "node": ">=16" + "node": ">=18" } } diff --git a/src/components/floatingActionButton.tsx b/src/components/floatingActionButton.tsx index 01ffafa..d6f6cc2 100644 --- a/src/components/floatingActionButton.tsx +++ b/src/components/floatingActionButton.tsx @@ -46,22 +46,22 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { { icon: 'tag', label: 'Tag', - onPress: () => navigate('Add Item'), + onPress: () => navigate('Add Tag'), }, { icon: 'note-text', label: 'Text', - onPress: () => navigate('Add Item'), + onPress: () => navigate('Add Meme'), }, { icon: 'image-album', label: 'Album', - onPress: () => navigate('Add Item'), + onPress: () => navigate('Add Meme'), }, ]} onStateChange={({ open }) => setState(open)} onPress={() => { - if (state) navigate('Add Item'); + if (state) navigate('Add Meme'); }} style={styles.fab} /> diff --git a/src/database/index.ts b/src/database/index.ts index e47d695..eabb69c 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -1,2 +1,2 @@ export { MEME_TYPE, memeTypePlural, Meme } from './meme'; -export { Tag } from './tag'; +export { Tag, deleteAllTags } from './tag'; diff --git a/src/database/tag.ts b/src/database/tag.ts index 7725cde..568bdd3 100644 --- a/src/database/tag.ts +++ b/src/database/tag.ts @@ -19,4 +19,10 @@ class Tag extends Realm.Object { }; } -export { Tag }; +const deleteAllTags = (realm: Realm) => { + realm.write(() => { + realm.delete(realm.objects('Tag')); + }); +}; + +export { Tag, deleteAllTags }; diff --git a/src/navigation.tsx b/src/navigation.tsx index b807d00..21f0e93 100644 --- a/src/navigation.tsx +++ b/src/navigation.tsx @@ -7,7 +7,7 @@ import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { BottomNavigation, Portal, useTheme } from 'react-native-paper'; -import { Home, Tags, Settings, AddItem } from './screens'; +import { Home, Tags, Settings, AddMeme, AddTag } from './screens'; import { horizontalScale } from './styles'; import { FloatingActionButton } from './components'; import { darkNavigationTheme, lightNavigationTheme } from './theme'; @@ -105,8 +105,13 @@ const NavigationContainer = () => { + diff --git a/src/screens/addItem.tsx b/src/screens/addItem.tsx deleted file mode 100644 index f33d1d1..0000000 --- a/src/screens/addItem.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { Text } from 'react-native-paper'; -import { PaddedView } from '../components'; - -const AddItem = () => { - return ( - - Add Item - - ); -}; - -export default AddItem; diff --git a/src/screens/addMeme.tsx b/src/screens/addMeme.tsx new file mode 100644 index 0000000..cc23a1d --- /dev/null +++ b/src/screens/addMeme.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Appbar, Text } from 'react-native-paper'; +import { PaddedView } from '../components'; +import { useNavigation } from '@react-navigation/native'; + +const AddMeme = () => { + const navigation = useNavigation(); + + return ( + <> + + navigation.goBack()} /> + + + + Add Meme + + + ); +}; + +export default AddMeme; diff --git a/src/screens/addTag.tsx b/src/screens/addTag.tsx new file mode 100644 index 0000000..91f5da0 --- /dev/null +++ b/src/screens/addTag.tsx @@ -0,0 +1,151 @@ +import React, { useState } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { + Chip, + TextInput, + Appbar, + HelperText, + Button, +} from 'react-native-paper'; +import { useNavigation } from '@react-navigation/native'; +import { BSON } from 'realm'; +import { useRealm } from '@realm/react'; +import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; +import { PaddedView } from '../components'; +import styles, { horizontalScale, verticalScale } from '../styles'; +import { + generateRandomColor, + getContrastColor, + isValidColor, +} from '../utilities'; + +const tagStyles = StyleSheet.create({ + preview: { + justifyContent: 'center', + flexDirection: 'row', + marginVertical: verticalScale(75), + }, + chip: { + padding: horizontalScale(5), + }, + chipText: { + fontSize: horizontalScale(15), + }, +}); + +const AddTag = () => { + const navigation = useNavigation(); + const realm = useRealm(); + + const [tagName, setTagName] = useState('newTag'); + const [tagColor, setTagColor] = useState(generateRandomColor()); + const [validatedTagColor, setValidatedTagColor] = useState(tagColor); + + const [tagNameError, setTagNameError] = useState(); + const [tagColorError, setTagColorError] = useState(); + + const handleTagNameChange = (name: string) => { + setTagName(name); + + if (name.length === 0) { + setTagNameError('Tag name cannot be empty'); + } else if (name.includes(' ')) { + setTagNameError('Tag name cannot contain spaces'); + } else { + // eslint-disable-next-line unicorn/no-useless-undefined + setTagNameError(undefined); + } + }; + + const handleTagColorChange = (color: string) => { + setTagColor(color); + + if (isValidColor(color)) { + setValidatedTagColor(color); + // eslint-disable-next-line unicorn/no-useless-undefined + setTagColorError(undefined); + } else { + setTagColorError('Color must be a valid hex or rgb value'); + } + }; + + const handleSave = () => { + realm.write(() => { + realm.create('Tag', { + id: new BSON.UUID(), + name: tagName, + color: tagColor, + memes: [], + }); + }); + navigation.goBack(); + }; + + return ( + <> + + navigation.goBack()} /> + + + + + + { + return ( + + ); + }} + elevated + style={[tagStyles.chip, { backgroundColor: validatedTagColor }]} + textStyle={[ + tagStyles.chipText, + { color: getContrastColor(validatedTagColor) }, + ]}> + {'#' + tagName} + + + + + {tagNameError} + + handleTagColorChange(generateRandomColor())} + /> + } + /> + + {tagColorError} + + + + + + ); +}; + +export default AddTag; diff --git a/src/screens/index.ts b/src/screens/index.ts index b04617f..a0ef6f1 100644 --- a/src/screens/index.ts +++ b/src/screens/index.ts @@ -1,4 +1,5 @@ -export { default as AddItem } from './addItem'; +export { default as AddMeme } from './addMeme'; +export { default as AddTag } from './addTag'; export { default as Home } from './home'; export { default as Settings } from './settings'; export { default as Tags } from './tags'; diff --git a/src/screens/tags.tsx b/src/screens/tags.tsx index 5588706..eac253d 100644 --- a/src/screens/tags.tsx +++ b/src/screens/tags.tsx @@ -1,12 +1,22 @@ import React from 'react'; -import { Text } from 'react-native-paper'; +import { Button, Text } from 'react-native-paper'; import { PaddedView } from '../components'; +import { useQuery, useRealm } from '@realm/react'; +import { Tag, deleteAllTags } from '../database'; const Tags = () => { + const realm = useRealm(); + const tags = useQuery('Tag'); + return ( - Tags + {tags.map(tag => ( + + {tag.name} + + ))} + ); }; diff --git a/src/styles.tsx b/src/styles.tsx index 47043d5..31ac461 100644 --- a/src/styles.tsx +++ b/src/styles.tsx @@ -47,6 +47,13 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'space-between', }, + flexColumn: { + flexDirection: 'column', + }, + flexColumnSpaceBetween: { + flexDirection: 'column', + justifyContent: 'space-between', + }, flexRowReverse: { flexDirection: 'row-reverse', }, diff --git a/src/utilities/color.ts b/src/utilities/color.ts new file mode 100644 index 0000000..88c0ce7 --- /dev/null +++ b/src/utilities/color.ts @@ -0,0 +1,46 @@ +import { darkTheme, lightTheme } from '../theme'; + +const getContrastColor = (hexColor: string) => { + if (hexColor.startsWith('#')) { + hexColor = hexColor.slice(1); + } + + const r = Number.parseInt(hexColor.slice(0, 2), 16); + const g = Number.parseInt(hexColor.slice(2, 4), 16); + const b = Number.parseInt(hexColor.slice(4, 6), 16); + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + + return brightness > 128 + ? lightTheme.colors.onSurface + : darkTheme.colors.onSurface; +}; + +const isHexColor = (color: string) => { + return /^#([\da-f]{6})$/i.test(color); +}; + +const isRgbColor = (color: string) => { + return /^rgb\((\d{1,3}), ?(\d{1,3}), ?(\d{1,3})\)$/i.test(color); +}; + +const isValidColor = (color: string) => { + return isHexColor(color) || isRgbColor(color); +}; + +const rgbToHex = (rgb: string) => { + const [r, g, b] = rgb + .replaceAll(/[^\d,]/g, '') + .split(',') + .map(value => Number.parseInt(value, 10)); + + return `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`; +}; + +const generateRandomColor = () => { + const r = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); + const g = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); + const b = Math.floor(Math.random() * 256).toString(16).padStart(2, '0'); + return `#${r}${g}${b}`; +}; + +export { getContrastColor, isHexColor, isRgbColor, isValidColor, rgbToHex, generateRandomColor }; diff --git a/src/utilities/index.ts b/src/utilities/index.ts index ec8de0f..1437e90 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -1,3 +1,11 @@ +export { + getContrastColor, + isHexColor, + isRgbColor, + isValidColor, + rgbToHex, + generateRandomColor +} from './color'; export { packageName, appName, fileProvider, noOp } from './constants'; export { isPermissionForPath, clearPermissions } from './permissions'; export { getSortIcon, getViewIcon } from './icon';