feat: basic implementation of IconPicker

This commit is contained in:
Xc165543337 2024-04-11 15:26:20 +02:00
parent c11f7c1474
commit 1fca00addb
4 changed files with 390 additions and 9 deletions

198
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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<HabitCreateFormProps> = ({ 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<HabitCreateFormProps> = ({ user }) => {
render={({ field: { onChange, value } }) => {
return (
<>
<Text style={[styles.spacing]}>Habit Type</Text>
<Text
style={[
styles.spacing,
{ justifyContent: "center", alignContent: "center" },
]}
>
Habit Type
<Tooltip
title="Routine habits are activities performed regularly, while Target habits involve setting specific objectives to be achieved through repeated actions."
enterTouchDelay={50}
leaveTouchDelay={25}
>
<IconButton
icon="chat-question-outline"
selected
size={24}
onPress={() => {}}
style={{ alignSelf: "center" }}
/>
</Tooltip>
</Text>
<SegmentedButtons
style={[{ width: "90%" }]}
onValueChange={onChange}
@ -207,13 +230,20 @@ export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
control={control}
render={({ field: { onChange, onBlur, value } }) => {
return (
<TextInput
placeholder="Icon"
onBlur={onBlur}
onChangeText={onChange}
// <TextInput
// placeholder="Icon"
// onBlur={onBlur}
// onChangeText={onChange}
// value={value}
// style={[styles.spacing, { width: "90%" }]}
// mode="outlined"
// />
<HabitIconSelectorModal
visible
value={value}
style={[styles.spacing, { width: "90%" }]}
mode="outlined"
onDismiss={() => {}}
onSelect={onChange}
onPress={() => {}}
/>
)
}}

View File

@ -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<HabitIconSelectorModalProps> = ({
visible = false,
value,
onDismiss,
onSelect,
onPress,
}) => {
const [selectedIcon, setSelectedIcon] = useState<string>()
const [possibleIcons, setPossibleIcons] = useState<IconDefinition[]>([])
const [searchText, setSearchText] = useState<string>(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 (
<>
<Button onPress={onPress}>Click to open icon list</Button>
<Modal
visible={visible}
onDismiss={onDismiss}
contentContainerStyle={styles.modalContent}
>
<Portal>
<View>
<ScrollView style={styles.scrollView}>
<List.Section title="Select an icon">
<View style={styles.iconContainer}>
<TextInput
label="Search"
value={searchText}
onChangeText={(text) => {
return setSearchText(text)
}}
/>
{possibleIcons.length > 0 ? (
<View>
{possibleIcons.map((icon) => {
return (
<IconButton
key={icon.iconName}
containerColor="white"
icon={({ size }) => {
return (
<FontAwesomeIcon
icon={icon}
size={size}
color={
selectedIcon === icon.iconName
? "blue"
: "black"
}
/>
)
}}
size={30}
onPress={() => {
handleIconSelect(icon.iconName)
}}
/>
)
})}
</View>
) : (
<View style={styles.noResults}>
<Text>No results found</Text>
</View>
)}
</View>
</List.Section>
</ScrollView>
</View>
</Portal>
</Modal>
</>
)
}
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",
},
})