From 1fca00addb04484cd0058b4ba845275e00795070 Mon Sep 17 00:00:00 2001 From: Xc165543337 <90028194+Xc165543337@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:26:20 +0200 Subject: [PATCH] feat: basic implementation of IconPicker --- package-lock.json | 198 +++++++++++++++++- package.json | 5 + .../HabitCreateForm/HabitCreateForm.tsx | 46 +++- .../HabitIconSelectorModal.tsx | 150 +++++++++++++ 4 files changed, 390 insertions(+), 9 deletions(-) create mode 100644 presentation/react/components/HabitCreateForm/HabitIconSelectorModal.tsx diff --git a/package-lock.json b/package-lock.json index 8f677ec..8613e33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,10 @@ "hasInstallScript": true, "dependencies": { "@expo/vector-icons": "14.0.0", + "@fortawesome/fontawesome-free": "6.5.2", + "@fortawesome/fontawesome-svg-core": "6.5.2", + "@fortawesome/free-solid-svg-icons": "6.5.2", + "@fortawesome/react-native-fontawesome": "0.3.0", "@hookform/resolvers": "3.3.4", "@react-native-async-storage/async-storage": "1.21.0", "@react-navigation/native": "6.1.16", @@ -34,6 +38,7 @@ "react-native-reanimated": "3.6.3", "react-native-safe-area-context": "4.8.2", "react-native-screens": "3.29.0", + "react-native-svg": "15.1.0", "react-native-url-polyfill": "2.0.0", "react-native-vector-icons": "10.0.3", "react-native-web": "0.19.10", @@ -4302,6 +4307,62 @@ "node": ">=8" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", + "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.2.tgz", + "integrity": "sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", + "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", + "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-native-fontawesome": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@fortawesome/react-native-fontawesome/-/react-native-fontawesome-0.3.0.tgz", + "integrity": "sha512-wSfetdK4+b/pvPbM2v+bZ5hfNlwtk9l3QuJo59sbMrxJalfX7BuF2WsSIWMSxfWwSsbOtY4+TUs6uw/rE59NJA==", + "dependencies": { + "humps": "^2.0.1", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react-native": ">= 0.67", + "react-native-svg": ">= 11.x" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -9597,6 +9658,11 @@ "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "node_modules/bplist-creator": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", @@ -10624,6 +10690,52 @@ "hyphenate-style-name": "^1.0.3" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -11162,6 +11274,30 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, "node_modules/domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -11175,6 +11311,33 @@ "node": ">=12" } }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -11251,7 +11414,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "engines": { "node": ">=0.12" }, @@ -13660,6 +13822,11 @@ "node": ">=16.17.0" } }, + "node_modules/humps": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz", + "integrity": "sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g==" + }, "node_modules/husky": { "version": "9.0.11", "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", @@ -17966,6 +18133,11 @@ "resolved": "https://registry.npmjs.org/md5hex/-/md5hex-1.0.0.tgz", "integrity": "sha512-c2YOUbp33+6thdCUi34xIyOU/a7bvGKj/3DB1iaPMTuPHf/Q2d5s4sn1FaCOO43XkXggnb08y5W2PU8UNYNLKQ==" }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" + }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", @@ -19051,6 +19223,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -20367,6 +20550,19 @@ "react-native": "*" } }, + "node_modules/react-native-svg": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.1.0.tgz", + "integrity": "sha512-p0Sx0EpQNk1nu6UcMEiB8K9P04n3J7s+pNYUwf1d/Yz+v4hk961VjuVqjyndgiEbHZyWiKWLZRVNuvLpwjPY2A==", + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-swipe-gestures": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/react-native-swipe-gestures/-/react-native-swipe-gestures-1.0.5.tgz", diff --git a/package.json b/package.json index df8a668..62ec942 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,10 @@ }, "dependencies": { "@expo/vector-icons": "14.0.0", + "@fortawesome/fontawesome-free": "6.5.2", + "@fortawesome/fontawesome-svg-core": "6.5.2", + "@fortawesome/free-solid-svg-icons": "6.5.2", + "@fortawesome/react-native-fontawesome": "0.3.0", "@hookform/resolvers": "3.3.4", "@react-native-async-storage/async-storage": "1.21.0", "@react-navigation/native": "6.1.16", @@ -44,6 +48,7 @@ "react-native-reanimated": "3.6.3", "react-native-safe-area-context": "4.8.2", "react-native-screens": "3.29.0", + "react-native-svg": "15.1.0", "react-native-url-polyfill": "2.0.0", "react-native-vector-icons": "10.0.3", "react-native-web": "0.19.10", diff --git a/presentation/react/components/HabitCreateForm/HabitCreateForm.tsx b/presentation/react/components/HabitCreateForm/HabitCreateForm.tsx index cf8e751..7b36efe 100644 --- a/presentation/react/components/HabitCreateForm/HabitCreateForm.tsx +++ b/presentation/react/components/HabitCreateForm/HabitCreateForm.tsx @@ -4,10 +4,12 @@ import { ScrollView, StyleSheet } from "react-native" import { Button, HelperText, + IconButton, SegmentedButtons, Snackbar, Text, TextInput, + Tooltip, } from "react-native-paper" import { SafeAreaView } from "react-native-safe-area-context" import ColorPicker, { @@ -23,6 +25,7 @@ import type { HabitCreateData } from "@/domain/entities/Habit" import { HabitCreateSchema } from "@/domain/entities/Habit" import type { User } from "@/domain/entities/User" import { useHabitsTracker } from "../../contexts/HabitsTracker" +import { HabitIconSelectorModal } from "./HabitIconSelectorModal" export interface HabitCreateFormProps { user: User @@ -43,7 +46,7 @@ export const HabitCreateForm: React.FC = ({ user }) => { userId: user.id, name: "", color: "#006CFF", - icon: "lightbulb", + icon: "circle-question", goal: { frequency: "daily", target: { @@ -164,7 +167,27 @@ export const HabitCreateForm: React.FC = ({ user }) => { render={({ field: { onChange, value } }) => { return ( <> - Habit Type + + Habit Type + + {}} + style={{ alignSelf: "center" }} + /> + + = ({ user }) => { control={control} render={({ field: { onChange, onBlur, value } }) => { return ( - + {}} + onSelect={onChange} + onPress={() => {}} /> ) }} diff --git a/presentation/react/components/HabitCreateForm/HabitIconSelectorModal.tsx b/presentation/react/components/HabitCreateForm/HabitIconSelectorModal.tsx new file mode 100644 index 0000000..90f558b --- /dev/null +++ b/presentation/react/components/HabitCreateForm/HabitIconSelectorModal.tsx @@ -0,0 +1,150 @@ +import React, { useState, useEffect } from "react" +import { + Modal, + IconButton, + Portal, + List, + Button, + TextInput, + Text, +} from "react-native-paper" +import { ScrollView, View, StyleSheet } from "react-native" +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome" +import type { + IconDefinition, + IconName, +} from "@fortawesome/fontawesome-svg-core" +import { library, findIconDefinition } from "@fortawesome/fontawesome-svg-core" +import { fas } from "@fortawesome/free-solid-svg-icons" + +export interface HabitIconSelectorModalProps { + visible?: boolean + value: string + onDismiss: () => void + onSelect: (icon: string) => void + onPress: () => void +} + +export const HabitIconSelectorModal: React.FC = ({ + visible = false, + value, + onDismiss, + onSelect, + onPress, +}) => { + const [selectedIcon, setSelectedIcon] = useState() + const [possibleIcons, setPossibleIcons] = useState([]) + const [searchText, setSearchText] = useState(value) + + useEffect(() => { + setPossibleIcons(findIconsInLibrary(searchText)) + }, [searchText]) + + library.add(fas) + + const handleIconSelect = (icon: string): void => { + setSelectedIcon(icon) + onSelect(icon) + } + + const findIconsInLibrary = (icon: string): IconDefinition[] => { + const iconNames = Object.keys(fas).map((key) => { + return fas[key]?.iconName ?? "" + }) + const matchedValue: string[] = iconNames.filter((name) => { + return name.includes(icon) + }) + return matchedValue.length > 0 + ? matchedValue.map((name) => { + return findIconDefinition({ + prefix: "fas", + iconName: name as IconName, + }) + }) + : [] + } + + return ( + <> + + + + + + + + { + return setSearchText(text) + }} + /> + {possibleIcons.length > 0 ? ( + + {possibleIcons.map((icon) => { + return ( + { + return ( + + ) + }} + size={30} + onPress={() => { + handleIconSelect(icon.iconName) + }} + /> + ) + })} + + ) : ( + + No results found + + )} + + + + + + + + ) +} + +const styles = StyleSheet.create({ + modalContent: { + backgroundColor: "white", + padding: 20, + margin: 20, + borderRadius: 10, + }, + iconContainer: { + flexDirection: "row", + flexWrap: "wrap", + justifyContent: "space-between", + }, + scrollView: { + maxHeight: 10000, + maxWidth: 5000, + }, + noResults: { + marginTop: 20, + alignItems: "center", + }, +})