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 1/3] 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", + }, +}) From 5e3cee079b08d9c944f3990556c7e401cd6fa1e7 Mon Sep 17 00:00:00 2001 From: Xc165543337 <90028194+Xc165543337@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:29:23 +0200 Subject: [PATCH 2/3] perf: optimize icon selector --- .../HabitCreateForm/HabitCreateForm.tsx | 10 +- .../HabitCreateForm/HabitIconList.tsx | 56 +++++++ .../HabitIconSelectorModal.tsx | 151 ++++++++---------- 3 files changed, 126 insertions(+), 91 deletions(-) create mode 100644 presentation/react/components/HabitCreateForm/HabitIconList.tsx diff --git a/presentation/react/components/HabitCreateForm/HabitCreateForm.tsx b/presentation/react/components/HabitCreateForm/HabitCreateForm.tsx index 7b36efe..2e9d408 100644 --- a/presentation/react/components/HabitCreateForm/HabitCreateForm.tsx +++ b/presentation/react/components/HabitCreateForm/HabitCreateForm.tsx @@ -228,16 +228,8 @@ export const HabitCreateForm: React.FC = ({ user }) => { { + render={({ field: { onChange, value } }) => { return ( - // void +} + +const HabitIconListWithoutMemo: React.FC = (props) => { + const { selectedIcon, possibleIcons, handleIconSelect } = props + if (possibleIcons.length > 0) { + return ( + + {possibleIcons.map((icon) => { + return ( + { + return ( + + ) + }} + size={30} + onPress={() => { + handleIconSelect(icon) + }} + /> + ) + })} + + ) + } + return ( + + No results found + + ) +} + +const styles = StyleSheet.create({ + noResults: { + marginTop: 20, + alignItems: "center", + }, +}) + +export const HabitIconList = memo(HabitIconListWithoutMemo) diff --git a/presentation/react/components/HabitCreateForm/HabitIconSelectorModal.tsx b/presentation/react/components/HabitCreateForm/HabitIconSelectorModal.tsx index 90f558b..be0fcc9 100644 --- a/presentation/react/components/HabitCreateForm/HabitIconSelectorModal.tsx +++ b/presentation/react/components/HabitCreateForm/HabitIconSelectorModal.tsx @@ -1,22 +1,16 @@ -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 { library } from "@fortawesome/fontawesome-svg-core" import { fas } from "@fortawesome/free-solid-svg-icons" +import React, { + memo, + useCallback, + useEffect, + useState, + useTransition, +} from "react" +import { ScrollView, StyleSheet, View } from "react-native" +import { Button, List, Modal, Portal, TextInput } from "react-native-paper" +import { HabitIconList } from "./HabitIconList" export interface HabitIconSelectorModalProps { visible?: boolean value: string @@ -25,6 +19,34 @@ export interface HabitIconSelectorModalProps { onPress: () => void } +interface SearchInputProps { + searchText: string + handleSearch: (text: string) => void +} + +library.add(fas) + +const iconNames = Object.keys(fas).map((key) => { + return fas[key]?.iconName ?? "" +}) + +const findIconsInLibrary = (icon: string): string[] => { + return iconNames + .filter((name, index, self) => { + return name.includes(icon) && self.indexOf(name) === index + }) + .slice(0, 20) +} + +const SearchInputWithoutMemo: React.FC = (props) => { + const { searchText, handleSearch } = props + return ( + + ) +} + +const SearchInput = memo(SearchInputWithoutMemo) + export const HabitIconSelectorModal: React.FC = ({ visible = false, value, @@ -33,36 +55,32 @@ export const HabitIconSelectorModal: React.FC = ({ onPress, }) => { const [selectedIcon, setSelectedIcon] = useState() - const [possibleIcons, setPossibleIcons] = useState([]) + const [possibleIcons, setPossibleIcons] = useState([]) const [searchText, setSearchText] = useState(value) + const [isPending, startTransition] = useTransition() + + const handleSearch = useCallback((text: string): void => { + setSearchText(text) + }, []) useEffect(() => { - setPossibleIcons(findIconsInLibrary(searchText)) - }, [searchText]) + const delay = setTimeout(() => { + startTransition(() => { + setPossibleIcons(findIconsInLibrary(searchText)) + }) + }, 500) + return () => { + return clearTimeout(delay) + } + }, [searchText, isPending]) - 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, - }) - }) - : [] - } + const handleIconSelect = useCallback( + (icon: string): void => { + setSelectedIcon(icon) + onSelect(icon) + }, + [onSelect], + ) return ( <> @@ -77,46 +95,15 @@ export const HabitIconSelectorModal: React.FC = ({ - { - return setSearchText(text) - }} + + - {possibleIcons.length > 0 ? ( - - {possibleIcons.map((icon) => { - return ( - { - return ( - - ) - }} - size={30} - onPress={() => { - handleIconSelect(icon.iconName) - }} - /> - ) - })} - - ) : ( - - No results found - - )} From 26b5a18edd400bcd8a146fc4d4ce37854a05776a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20LUDWIG?= Date: Fri, 12 Apr 2024 23:13:38 +0200 Subject: [PATCH 3/3] feat: icon picker complete --- .env.example | 2 +- README.md | 2 +- app/_layout.tsx | 4 + app/application/habits/[habitId]/index.tsx | 2 +- app/application/habits/new.tsx | 2 +- infrastructure/supabase/seed.sql | 4 +- package-lock.json | 81 ++++++----- package.json | 8 +- .../HabitCreateForm/HabitIconList.tsx | 56 ------- .../HabitIconSelectorModal.tsx | 137 ------------------ .../HabitCreateForm.tsx | 53 +++++-- .../HabitEditForm.tsx | 44 ++++-- .../HabitForm/IconSelectorModal.tsx | 121 ++++++++++++++++ .../react/components/HabitForm/IconsList.tsx | 74 ++++++++++ .../components/HabitsMainPage/HabitCard.tsx | 9 +- 15 files changed, 326 insertions(+), 273 deletions(-) delete mode 100644 presentation/react/components/HabitCreateForm/HabitIconList.tsx delete mode 100644 presentation/react/components/HabitCreateForm/HabitIconSelectorModal.tsx rename presentation/react/components/{HabitCreateForm => HabitForm}/HabitCreateForm.tsx (83%) rename presentation/react/components/{HabitEditForm => HabitForm}/HabitEditForm.tsx (74%) create mode 100644 presentation/react/components/HabitForm/IconSelectorModal.tsx create mode 100644 presentation/react/components/HabitForm/IconsList.tsx diff --git a/.env.example b/.env.example index c53e3c2..0196b4d 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # Supabase - Local -# EXPO_PUBLIC_SUPABASE_URL='http://127.0.0.1:54321' # Replace `127.0.0.1` with local IP (`hostname -I` on Linux) +# EXPO_PUBLIC_SUPABASE_URL='http://127.0.0.1:54321' # Replace `127.0.0.1` with local IP (e.g: `hostname -i` on GNU/Linux) # EXPO_PUBLIC_SUPABASE_ANON_KEY='' # Supabase - Production diff --git a/README.md b/README.md index ff85d05..a30b50c 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ npm run start Ce n'est pas strictement nécessaire pour le développement de l'application (même si recommandé), de lancer [Supabase](https://supabase.io/) en local, car l'application est déjà déployée sur un serveur [Supabase](https://supabase.io/) en production (`.env.example` est pré-configuré avec cet environnement). ```sh -npm run supabase +npm run supabase start ``` #### Principales Commandes Supabase diff --git a/app/_layout.tsx b/app/_layout.tsx index 5130169..e4616e8 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,4 +1,6 @@ import { Stack } from "expo-router" +import { fas } from "@fortawesome/free-solid-svg-icons" +import { library } from "@fortawesome/fontawesome-svg-core" import * as SplashScreen from "expo-splash-screen" import { MD3LightTheme as DefaultTheme, @@ -20,6 +22,8 @@ export const unstableSettings = { initialRouteName: "index", } +library.add(fas) + SplashScreen.preventAutoHideAsync().catch((error) => { console.error(error) }) diff --git a/app/application/habits/[habitId]/index.tsx b/app/application/habits/[habitId]/index.tsx index 02c162c..f1f5fd4 100644 --- a/app/application/habits/[habitId]/index.tsx +++ b/app/application/habits/[habitId]/index.tsx @@ -1,6 +1,6 @@ import { Redirect, useLocalSearchParams } from "expo-router" -import { HabitEditForm } from "@/presentation/react/components/HabitEditForm/HabitEditForm" +import { HabitEditForm } from "@/presentation/react/components/HabitForm/HabitEditForm" import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker" const HabitPage: React.FC = () => { diff --git a/app/application/habits/new.tsx b/app/application/habits/new.tsx index f410ab0..9f99f8d 100644 --- a/app/application/habits/new.tsx +++ b/app/application/habits/new.tsx @@ -1,4 +1,4 @@ -import { HabitCreateForm } from "@/presentation/react/components/HabitCreateForm/HabitCreateForm" +import { HabitCreateForm } from "@/presentation/react/components/HabitForm/HabitCreateForm" import { useAuthentication } from "@/presentation/react/contexts/Authentication" const NewHabitPage: React.FC = () => { diff --git a/infrastructure/supabase/seed.sql b/infrastructure/supabase/seed.sql index 97c607b..d6193fa 100644 --- a/infrastructure/supabase/seed.sql +++ b/infrastructure/supabase/seed.sql @@ -263,5 +263,5 @@ VALUES 4733 ); --- SELECT setval('habits_id_seq', (SELECT coalesce(MAX(id) + 1, 1) FROM habits), false); --- SELECT setval('habits_progresses_id_seq', (SELECT coalesce(MAX(id) + 1, 1) FROM habits_progresses), false); +SELECT setval('habits_id_seq', (SELECT coalesce(MAX(id) + 1, 1) FROM habits), false); +SELECT setval('habits_progresses_id_seq', (SELECT coalesce(MAX(id) + 1, 1) FROM habits_progresses), false); diff --git a/package-lock.json b/package-lock.json index 3ee8f9b..9616040 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,14 +10,13 @@ "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", - "@supabase/supabase-js": "2.42.1", + "@supabase/supabase-js": "2.42.3", "expo": "50.0.15", "expo-font": "11.10.3", "expo-linking": "6.2.2", @@ -30,7 +29,7 @@ "lottie-react-native": "6.5.1", "react": "18.2.0", "react-dom": "18.2.0", - "react-hook-form": "7.51.2", + "react-hook-form": "7.51.3", "react-native": "0.73.6", "react-native-calendars": "1.1304.1", "react-native-elements": "3.4.3", @@ -39,7 +38,6 @@ "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", @@ -55,7 +53,7 @@ "@tsconfig/strictest": "2.0.5", "@types/jest": "29.5.12", "@types/node": "20.12.7", - "@types/react": "18.2.76", + "@types/react": "18.2.77", "@types/react-test-renderer": "18.0.7", "@typescript-eslint/eslint-plugin": "7.6.0", "@typescript-eslint/parser": "7.6.0", @@ -4317,15 +4315,6 @@ "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", @@ -8031,17 +8020,17 @@ } }, "node_modules/@supabase/postgrest-js": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.1.tgz", - "integrity": "sha512-H/4PABAAgMrEIo7oewUZiZMy422Pgc19OLVjU8Vwopcvfr3GD7h8Re4VXtiaPPZqc/2z/k3PnyguFnoKIB7fkA==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.2.tgz", + "integrity": "sha512-9/7pUmXExvGuEK1yZhVYXPZnLEkDTwxgMQHXLrN5BwPZZm4iUCL1YEyep/Z2lIZah8d8M433mVAUEGsihUj5KQ==", "dependencies": { "@supabase/node-fetch": "^2.6.14" } }, "node_modules/@supabase/realtime-js": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.9.3.tgz", - "integrity": "sha512-lAp50s2n3FhGJFq+wTSXLNIDPw5Y0Wxrgt44eM5nLSA3jZNUUP3Oq2Ccd1CbZdVntPCWLZvJaU//pAd2NE+QnQ==", + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.9.4.tgz", + "integrity": "sha512-wdq+2hZpgw0r2ldRs87d3U08Y8BrsO1bZxPNqbImpYshAEkusDz4vufR8KaqujKxqewmXS6YnUhuRVdvSEIKCA==", "dependencies": { "@supabase/node-fetch": "^2.6.14", "@types/phoenix": "^1.5.4", @@ -8058,15 +8047,15 @@ } }, "node_modules/@supabase/supabase-js": { - "version": "2.42.1", - "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.42.1.tgz", - "integrity": "sha512-y05XIC6wChApz8BWE2TLUo6SBGp9ttbjhQ9DcQXI897vI3RRPIjm3wZqUoZiexUco+kgt3Em53+m55nB8Um6Sg==", + "version": "2.42.3", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.42.3.tgz", + "integrity": "sha512-/o52L/ngsGapcOUygWigvxzBo/bUVM4bubZMsUSZqZc+9sgjXZsgP/cwyggWUv3QOIqmbBrfSPgDLUh+Ofgi7Q==", "dependencies": { "@supabase/auth-js": "2.63.0", "@supabase/functions-js": "2.2.2", "@supabase/node-fetch": "2.6.15", - "@supabase/postgrest-js": "1.15.1", - "@supabase/realtime-js": "2.9.3", + "@supabase/postgrest-js": "1.15.2", + "@supabase/realtime-js": "2.9.4", "@supabase/storage-js": "2.5.5" } }, @@ -8260,9 +8249,9 @@ "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/react": { - "version": "18.2.76", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.76.tgz", - "integrity": "sha512-T6z/v7YxpswDM61Vq5KoSPTJqCkroJfsDIsvXCr4+qOY6gik5Ju4w0jf67cpC5z7ydOnp/E0V0W08pDRy8u9Xw==", + "version": "18.2.77", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.77.tgz", + "integrity": "sha512-CUT9KUUF+HytDM7WiXKLF9qUSg4tGImwy4FXTlfEDPEkkNUzJ7rVFolYweJ9fS1ljoIaP7M7Rdjc5eUm/Yu5AA==", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -9660,7 +9649,8 @@ "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==" + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "peer": true }, "node_modules/bplist-creator": { "version": "0.1.0", @@ -9947,9 +9937,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001608", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001608.tgz", - "integrity": "sha512-cjUJTQkk9fQlJR2s4HMuPMvTiRggl0rAVMtthQuyOlDWuqHXqN8azLq+pi8B2TjwKJ32diHjUqRIKeFX4z1FoA==", + "version": "1.0.30001609", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001609.tgz", + "integrity": "sha512-JFPQs34lHKx1B5t1EpQpWH4c+29zIyn/haGsbpfq3suuV9v56enjFt23zqijxGTMwy1p/4H2tjnQMY+p1WoAyA==", "funding": [ { "type": "opencollective", @@ -10693,6 +10683,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "peer": true, "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", @@ -10708,6 +10699,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "peer": true, "dependencies": { "mdn-data": "2.0.14", "source-map": "^0.6.1" @@ -10720,6 +10712,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -10728,6 +10721,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "peer": true, "engines": { "node": ">= 6" }, @@ -11273,6 +11267,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "peer": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -11291,7 +11286,8 @@ "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "peer": true }, "node_modules/domexception": { "version": "4.0.0", @@ -11310,6 +11306,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "peer": true, "dependencies": { "domelementtype": "^2.3.0" }, @@ -11324,6 +11321,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "peer": true, "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -11376,9 +11374,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.733", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.733.tgz", - "integrity": "sha512-gUI9nhI2iBGF0OaYYLKOaOtliFMl+Bt1rY7VmEjwxOxqoYLub/D9xmduPEhbw2imE6gYkJKhIE5it+KE2ulVxQ==" + "version": "1.4.735", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.735.tgz", + "integrity": "sha512-pkYpvwg8VyOTQAeBqZ7jsmpCjko1Qc6We1ZtZCjRyYbT5v4AIUKDy5cQTRotQlSSZmMr8jqpEt6JtOj5k7lR7A==" }, "node_modules/emittery": { "version": "0.13.1", @@ -18140,7 +18138,8 @@ "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==" + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "peer": true }, "node_modules/memoize-one": { "version": "5.2.1", @@ -19231,6 +19230,7 @@ "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==", + "peer": true, "dependencies": { "boolbase": "^1.0.0" }, @@ -20329,9 +20329,9 @@ } }, "node_modules/react-hook-form": { - "version": "7.51.2", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.2.tgz", - "integrity": "sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==", + "version": "7.51.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz", + "integrity": "sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==", "engines": { "node": ">=12.22.0" }, @@ -20556,6 +20556,7 @@ "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==", + "peer": true, "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3" diff --git a/package.json b/package.json index 8ad96fe..52484ea 100644 --- a/package.json +++ b/package.json @@ -20,14 +20,13 @@ }, "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", - "@supabase/supabase-js": "2.42.1", + "@supabase/supabase-js": "2.42.3", "expo": "50.0.15", "expo-font": "11.10.3", "expo-linking": "6.2.2", @@ -40,7 +39,7 @@ "lottie-react-native": "6.5.1", "react": "18.2.0", "react-dom": "18.2.0", - "react-hook-form": "7.51.2", + "react-hook-form": "7.51.3", "react-native": "0.73.6", "react-native-calendars": "1.1304.1", "react-native-elements": "3.4.3", @@ -49,7 +48,6 @@ "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", @@ -65,7 +63,7 @@ "@tsconfig/strictest": "2.0.5", "@types/jest": "29.5.12", "@types/node": "20.12.7", - "@types/react": "18.2.76", + "@types/react": "18.2.77", "@types/react-test-renderer": "18.0.7", "@typescript-eslint/eslint-plugin": "7.6.0", "@typescript-eslint/parser": "7.6.0", diff --git a/presentation/react/components/HabitCreateForm/HabitIconList.tsx b/presentation/react/components/HabitCreateForm/HabitIconList.tsx deleted file mode 100644 index 471f5fa..0000000 --- a/presentation/react/components/HabitCreateForm/HabitIconList.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import type { IconName } from "@fortawesome/fontawesome-svg-core" -import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome" -import React, { memo } from "react" -import { View, StyleSheet } from "react-native" -import { IconButton, Text } from "react-native-paper" - -export interface HabitIconListProps { - selectedIcon?: string - possibleIcons: string[] - handleIconSelect: (icon: string) => void -} - -const HabitIconListWithoutMemo: React.FC = (props) => { - const { selectedIcon, possibleIcons, handleIconSelect } = props - if (possibleIcons.length > 0) { - return ( - - {possibleIcons.map((icon) => { - return ( - { - return ( - - ) - }} - size={30} - onPress={() => { - handleIconSelect(icon) - }} - /> - ) - })} - - ) - } - return ( - - No results found - - ) -} - -const styles = StyleSheet.create({ - noResults: { - marginTop: 20, - alignItems: "center", - }, -}) - -export const HabitIconList = memo(HabitIconListWithoutMemo) diff --git a/presentation/react/components/HabitCreateForm/HabitIconSelectorModal.tsx b/presentation/react/components/HabitCreateForm/HabitIconSelectorModal.tsx deleted file mode 100644 index be0fcc9..0000000 --- a/presentation/react/components/HabitCreateForm/HabitIconSelectorModal.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { library } from "@fortawesome/fontawesome-svg-core" -import { fas } from "@fortawesome/free-solid-svg-icons" -import React, { - memo, - useCallback, - useEffect, - useState, - useTransition, -} from "react" -import { ScrollView, StyleSheet, View } from "react-native" -import { Button, List, Modal, Portal, TextInput } from "react-native-paper" - -import { HabitIconList } from "./HabitIconList" -export interface HabitIconSelectorModalProps { - visible?: boolean - value: string - onDismiss: () => void - onSelect: (icon: string) => void - onPress: () => void -} - -interface SearchInputProps { - searchText: string - handleSearch: (text: string) => void -} - -library.add(fas) - -const iconNames = Object.keys(fas).map((key) => { - return fas[key]?.iconName ?? "" -}) - -const findIconsInLibrary = (icon: string): string[] => { - return iconNames - .filter((name, index, self) => { - return name.includes(icon) && self.indexOf(name) === index - }) - .slice(0, 20) -} - -const SearchInputWithoutMemo: React.FC = (props) => { - const { searchText, handleSearch } = props - return ( - - ) -} - -const SearchInput = memo(SearchInputWithoutMemo) - -export const HabitIconSelectorModal: React.FC = ({ - visible = false, - value, - onDismiss, - onSelect, - onPress, -}) => { - const [selectedIcon, setSelectedIcon] = useState() - const [possibleIcons, setPossibleIcons] = useState([]) - const [searchText, setSearchText] = useState(value) - const [isPending, startTransition] = useTransition() - - const handleSearch = useCallback((text: string): void => { - setSearchText(text) - }, []) - - useEffect(() => { - const delay = setTimeout(() => { - startTransition(() => { - setPossibleIcons(findIconsInLibrary(searchText)) - }) - }, 500) - return () => { - return clearTimeout(delay) - } - }, [searchText, isPending]) - - const handleIconSelect = useCallback( - (icon: string): void => { - setSelectedIcon(icon) - onSelect(icon) - }, - [onSelect], - ) - - return ( - <> - - - - - - - - - - - - - - - - - ) -} - -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", - }, -}) diff --git a/presentation/react/components/HabitCreateForm/HabitCreateForm.tsx b/presentation/react/components/HabitForm/HabitCreateForm.tsx similarity index 83% rename from presentation/react/components/HabitCreateForm/HabitCreateForm.tsx rename to presentation/react/components/HabitForm/HabitCreateForm.tsx index 2e9d408..deebdd8 100644 --- a/presentation/react/components/HabitCreateForm/HabitCreateForm.tsx +++ b/presentation/react/components/HabitForm/HabitCreateForm.tsx @@ -1,15 +1,16 @@ +import type { IconName } from "@fortawesome/free-solid-svg-icons" +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome" import { zodResolver } from "@hookform/resolvers/zod" +import { useState } from "react" import { Controller, useForm } from "react-hook-form" -import { ScrollView, StyleSheet } from "react-native" +import { ScrollView, StyleSheet, View } 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, { @@ -17,7 +18,6 @@ import ColorPicker, { Panel1, Preview, } from "reanimated-color-picker" -import { useState } from "react" import type { GoalFrequency, GoalType } from "@/domain/entities/Goal" import { GOAL_FREQUENCIES, GOAL_TYPES } from "@/domain/entities/Goal" @@ -25,7 +25,8 @@ 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" +import { useBoolean } from "../../hooks/useBoolean" +import { IconSelectorModal } from "./IconSelectorModal" export interface HabitCreateFormProps { user: User @@ -58,6 +59,12 @@ export const HabitCreateForm: React.FC = ({ user }) => { const [isVisibleSnackbar, setIsVisibleSnackbar] = useState(false) + const { + value: isModalIconSelectorVisible, + setTrue: openModalIconSelector, + setFalse: closeModalIconSelector, + } = useBoolean() + const onDismissSnackbar = (): void => { setIsVisibleSnackbar(false) } @@ -65,6 +72,7 @@ export const HabitCreateForm: React.FC = ({ user }) => { const onSubmit = async (data: HabitCreateData): Promise => { await habitsTrackerPresenter.habitCreate(data) setIsVisibleSnackbar(true) + closeModalIconSelector() reset() } @@ -174,7 +182,7 @@ export const HabitCreateForm: React.FC = ({ user }) => { ]} > Habit Type - = ({ user }) => { onPress={() => {}} style={{ alignSelf: "center" }} /> - + */} = ({ user }) => { control={control} render={({ field: { onChange, value } }) => { return ( - {}} - onSelect={onChange} - onPress={() => {}} - /> + + + + + + ) }} name="icon" @@ -247,7 +270,7 @@ export const HabitCreateForm: React.FC = ({ user }) => { onPress={handleSubmit(onSubmit)} loading={habitCreate.state === "loading"} disabled={habitCreate.state === "loading"} - style={[styles.spacing, { width: "90%" }]} + style={[styles.spacing, { width: "100%" }]} > Create your habit! 🚀 diff --git a/presentation/react/components/HabitEditForm/HabitEditForm.tsx b/presentation/react/components/HabitForm/HabitEditForm.tsx similarity index 74% rename from presentation/react/components/HabitEditForm/HabitEditForm.tsx rename to presentation/react/components/HabitForm/HabitEditForm.tsx index be69d7f..844492a 100644 --- a/presentation/react/components/HabitEditForm/HabitEditForm.tsx +++ b/presentation/react/components/HabitForm/HabitEditForm.tsx @@ -1,7 +1,9 @@ +import type { IconName } from "@fortawesome/free-solid-svg-icons" +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome" import { zodResolver } from "@hookform/resolvers/zod" import { useState } from "react" import { Controller, useForm } from "react-hook-form" -import { ScrollView, StyleSheet } from "react-native" +import { ScrollView, StyleSheet, View } from "react-native" import { Button, HelperText, Snackbar, TextInput } from "react-native-paper" import { SafeAreaView } from "react-native-safe-area-context" import ColorPicker, { @@ -13,6 +15,8 @@ import ColorPicker, { import type { Habit, HabitEditData } from "@/domain/entities/Habit" import { HabitEditSchema } from "@/domain/entities/Habit" import { useHabitsTracker } from "../../contexts/HabitsTracker" +import { useBoolean } from "../../hooks/useBoolean" +import { IconSelectorModal } from "./IconSelectorModal" export interface HabitEditFormProps { habit: Habit @@ -37,6 +41,12 @@ export const HabitEditForm: React.FC = ({ habit }) => { }, }) + const { + value: isModalIconSelectorVisible, + setTrue: openModalIconSelector, + setFalse: closeModalIconSelector, + } = useBoolean() + const [isVisibleSnackbar, setIsVisibleSnackbar] = useState(false) const onDismissSnackbar = (): void => { @@ -110,16 +120,30 @@ export const HabitEditForm: React.FC = ({ habit }) => { { + render={({ field: { onChange, value } }) => { return ( - + + + + + + ) }} name="icon" diff --git a/presentation/react/components/HabitForm/IconSelectorModal.tsx b/presentation/react/components/HabitForm/IconSelectorModal.tsx new file mode 100644 index 0000000..20389c6 --- /dev/null +++ b/presentation/react/components/HabitForm/IconSelectorModal.tsx @@ -0,0 +1,121 @@ +import { fas } from "@fortawesome/free-solid-svg-icons" +import { memo, useCallback, useEffect, useState, useTransition } from "react" +import { Modal, ScrollView, View } from "react-native" +import { Button, List, Text, TextInput } from "react-native-paper" +import type { IconName } from "@fortawesome/fontawesome-svg-core" +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome" + +import { IconsList } from "./IconsList" + +export interface IconSelectorModalProps { + isVisible?: boolean + selectedIcon?: string + onIconSelect: (icon: string) => void + handleCloseModal?: () => void +} + +interface SearchInputProps { + searchText: string + handleSearch: (text: string) => void +} +const SearchInputWithoutMemo: React.FC = (props) => { + const { searchText, handleSearch } = props + return ( + + ) +} +const SearchInput = memo(SearchInputWithoutMemo) + +const iconNames = Object.keys(fas).map((key) => { + return fas[key]?.iconName ?? key +}) + +const findIconsInLibrary = (icon: string): string[] => { + return iconNames + .filter((name, index, self) => { + return name.includes(icon) && self.indexOf(name) === index + }) + .slice(0, 50) +} + +export const IconSelectorModal: React.FC = ({ + isVisible = false, + selectedIcon, + onIconSelect, + handleCloseModal, +}) => { + const [possibleIcons, setPossibleIcons] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [searchText, setSearchText] = useState("") + const [_isPending, startTransition] = useTransition() + + const handleSearch = useCallback((text: string): void => { + setSearchText(text) + }, []) + + useEffect(() => { + const handlePossibleIcons = (): void => { + startTransition(() => { + setPossibleIcons(findIconsInLibrary(searchText)) + setIsLoading(false) + }) + } + const debounceHandleSearch = setTimeout(handlePossibleIcons, 400) + + return () => { + return clearTimeout(debounceHandleSearch) + } + }, [searchText]) + + const handleIconSelect = useCallback( + (icon: string): void => { + onIconSelect(icon) + }, + [onIconSelect], + ) + + return ( + + + + + Selected Icon: + + + + + + + + + + + + + + + ) +} diff --git a/presentation/react/components/HabitForm/IconsList.tsx b/presentation/react/components/HabitForm/IconsList.tsx new file mode 100644 index 0000000..16b299a --- /dev/null +++ b/presentation/react/components/HabitForm/IconsList.tsx @@ -0,0 +1,74 @@ +import type { IconName } from "@fortawesome/fontawesome-svg-core" +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome" +import React, { memo } from "react" +import { View } from "react-native" +import { ActivityIndicator, IconButton, Text } from "react-native-paper" + +export interface IconsListProps { + selectedIcon?: string + possibleIcons: string[] + isLoading?: boolean + handleIconSelect: (icon: string) => void +} + +const IconsListWithoutMemo: React.FC = (props) => { + const { + selectedIcon, + possibleIcons, + isLoading = false, + handleIconSelect, + } = props + + if (possibleIcons.length <= 0) { + return ( + + {isLoading ? ( + + ) : ( + No results found + )} + + ) + } + + return ( + + {possibleIcons.map((icon) => { + return ( + { + return ( + + ) + }} + size={30} + onPress={() => { + handleIconSelect(icon) + }} + /> + ) + })} + + ) +} + +export const IconsList = memo(IconsListWithoutMemo) diff --git a/presentation/react/components/HabitsMainPage/HabitCard.tsx b/presentation/react/components/HabitsMainPage/HabitCard.tsx index 8fa1a39..4f032d3 100644 --- a/presentation/react/components/HabitsMainPage/HabitCard.tsx +++ b/presentation/react/components/HabitsMainPage/HabitCard.tsx @@ -1,15 +1,16 @@ -import FontAwesome6 from "@expo/vector-icons/FontAwesome6" +import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome" import { useRouter } from "expo-router" import { useState } from "react" import { View } from "react-native" import { Checkbox, List, Text } from "react-native-paper" import type LottieView from "lottie-react-native" +import type { IconName } from "@fortawesome/free-solid-svg-icons" import type { GoalBoolean } from "@/domain/entities/Goal" import { GoalBooleanProgress } from "@/domain/entities/Goal" import type { HabitHistory } from "@/domain/entities/HabitHistory" import { getColorRGBAFromHex } from "@/utils/colors" -import { useHabitsTracker } from "../../contexts/HabitsTracker" +import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker" export interface HabitCardProps { habitHistory: HabitHistory @@ -65,9 +66,9 @@ export const HabitCard: React.FC = (props) => { left={() => { return ( -