feat: icon picker complete

This commit is contained in:
Théo LUDWIG 2024-04-12 23:13:38 +02:00
parent 5e3cee079b
commit 26b5a18edd
Signed by: theoludwig
GPG Key ID: ADFE5A563D718F3B
15 changed files with 326 additions and 273 deletions

View File

@ -1,5 +1,5 @@
# Supabase - Local # 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='' # EXPO_PUBLIC_SUPABASE_ANON_KEY=''
# Supabase - Production # Supabase - Production

View File

@ -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). 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 ```sh
npm run supabase npm run supabase start
``` ```
#### Principales Commandes Supabase #### Principales Commandes Supabase

View File

@ -1,4 +1,6 @@
import { Stack } from "expo-router" 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 * as SplashScreen from "expo-splash-screen"
import { import {
MD3LightTheme as DefaultTheme, MD3LightTheme as DefaultTheme,
@ -20,6 +22,8 @@ export const unstableSettings = {
initialRouteName: "index", initialRouteName: "index",
} }
library.add(fas)
SplashScreen.preventAutoHideAsync().catch((error) => { SplashScreen.preventAutoHideAsync().catch((error) => {
console.error(error) console.error(error)
}) })

View File

@ -1,6 +1,6 @@
import { Redirect, useLocalSearchParams } from "expo-router" 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" import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker"
const HabitPage: React.FC = () => { const HabitPage: React.FC = () => {

View File

@ -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" import { useAuthentication } from "@/presentation/react/contexts/Authentication"
const NewHabitPage: React.FC = () => { const NewHabitPage: React.FC = () => {

View File

@ -263,5 +263,5 @@ VALUES
4733 4733
); );
-- SELECT setval('habits_id_seq', (SELECT coalesce(MAX(id) + 1, 1) FROM habits), 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); SELECT setval('habits_progresses_id_seq', (SELECT coalesce(MAX(id) + 1, 1) FROM habits_progresses), false);

81
package-lock.json generated
View File

@ -10,14 +10,13 @@
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@expo/vector-icons": "14.0.0", "@expo/vector-icons": "14.0.0",
"@fortawesome/fontawesome-free": "6.5.2",
"@fortawesome/fontawesome-svg-core": "6.5.2", "@fortawesome/fontawesome-svg-core": "6.5.2",
"@fortawesome/free-solid-svg-icons": "6.5.2", "@fortawesome/free-solid-svg-icons": "6.5.2",
"@fortawesome/react-native-fontawesome": "0.3.0", "@fortawesome/react-native-fontawesome": "0.3.0",
"@hookform/resolvers": "3.3.4", "@hookform/resolvers": "3.3.4",
"@react-native-async-storage/async-storage": "1.21.0", "@react-native-async-storage/async-storage": "1.21.0",
"@react-navigation/native": "6.1.16", "@react-navigation/native": "6.1.16",
"@supabase/supabase-js": "2.42.1", "@supabase/supabase-js": "2.42.3",
"expo": "50.0.15", "expo": "50.0.15",
"expo-font": "11.10.3", "expo-font": "11.10.3",
"expo-linking": "6.2.2", "expo-linking": "6.2.2",
@ -30,7 +29,7 @@
"lottie-react-native": "6.5.1", "lottie-react-native": "6.5.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "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": "0.73.6",
"react-native-calendars": "1.1304.1", "react-native-calendars": "1.1304.1",
"react-native-elements": "3.4.3", "react-native-elements": "3.4.3",
@ -39,7 +38,6 @@
"react-native-reanimated": "3.6.3", "react-native-reanimated": "3.6.3",
"react-native-safe-area-context": "4.8.2", "react-native-safe-area-context": "4.8.2",
"react-native-screens": "3.29.0", "react-native-screens": "3.29.0",
"react-native-svg": "15.1.0",
"react-native-url-polyfill": "2.0.0", "react-native-url-polyfill": "2.0.0",
"react-native-vector-icons": "10.0.3", "react-native-vector-icons": "10.0.3",
"react-native-web": "0.19.10", "react-native-web": "0.19.10",
@ -55,7 +53,7 @@
"@tsconfig/strictest": "2.0.5", "@tsconfig/strictest": "2.0.5",
"@types/jest": "29.5.12", "@types/jest": "29.5.12",
"@types/node": "20.12.7", "@types/node": "20.12.7",
"@types/react": "18.2.76", "@types/react": "18.2.77",
"@types/react-test-renderer": "18.0.7", "@types/react-test-renderer": "18.0.7",
"@typescript-eslint/eslint-plugin": "7.6.0", "@typescript-eslint/eslint-plugin": "7.6.0",
"@typescript-eslint/parser": "7.6.0", "@typescript-eslint/parser": "7.6.0",
@ -4317,15 +4315,6 @@
"node": ">=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": { "node_modules/@fortawesome/fontawesome-svg-core": {
"version": "6.5.2", "version": "6.5.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", "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": { "node_modules/@supabase/postgrest-js": {
"version": "1.15.1", "version": "1.15.2",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.1.tgz", "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.15.2.tgz",
"integrity": "sha512-H/4PABAAgMrEIo7oewUZiZMy422Pgc19OLVjU8Vwopcvfr3GD7h8Re4VXtiaPPZqc/2z/k3PnyguFnoKIB7fkA==", "integrity": "sha512-9/7pUmXExvGuEK1yZhVYXPZnLEkDTwxgMQHXLrN5BwPZZm4iUCL1YEyep/Z2lIZah8d8M433mVAUEGsihUj5KQ==",
"dependencies": { "dependencies": {
"@supabase/node-fetch": "^2.6.14" "@supabase/node-fetch": "^2.6.14"
} }
}, },
"node_modules/@supabase/realtime-js": { "node_modules/@supabase/realtime-js": {
"version": "2.9.3", "version": "2.9.4",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.9.3.tgz", "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.9.4.tgz",
"integrity": "sha512-lAp50s2n3FhGJFq+wTSXLNIDPw5Y0Wxrgt44eM5nLSA3jZNUUP3Oq2Ccd1CbZdVntPCWLZvJaU//pAd2NE+QnQ==", "integrity": "sha512-wdq+2hZpgw0r2ldRs87d3U08Y8BrsO1bZxPNqbImpYshAEkusDz4vufR8KaqujKxqewmXS6YnUhuRVdvSEIKCA==",
"dependencies": { "dependencies": {
"@supabase/node-fetch": "^2.6.14", "@supabase/node-fetch": "^2.6.14",
"@types/phoenix": "^1.5.4", "@types/phoenix": "^1.5.4",
@ -8058,15 +8047,15 @@
} }
}, },
"node_modules/@supabase/supabase-js": { "node_modules/@supabase/supabase-js": {
"version": "2.42.1", "version": "2.42.3",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.42.1.tgz", "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.42.3.tgz",
"integrity": "sha512-y05XIC6wChApz8BWE2TLUo6SBGp9ttbjhQ9DcQXI897vI3RRPIjm3wZqUoZiexUco+kgt3Em53+m55nB8Um6Sg==", "integrity": "sha512-/o52L/ngsGapcOUygWigvxzBo/bUVM4bubZMsUSZqZc+9sgjXZsgP/cwyggWUv3QOIqmbBrfSPgDLUh+Ofgi7Q==",
"dependencies": { "dependencies": {
"@supabase/auth-js": "2.63.0", "@supabase/auth-js": "2.63.0",
"@supabase/functions-js": "2.2.2", "@supabase/functions-js": "2.2.2",
"@supabase/node-fetch": "2.6.15", "@supabase/node-fetch": "2.6.15",
"@supabase/postgrest-js": "1.15.1", "@supabase/postgrest-js": "1.15.2",
"@supabase/realtime-js": "2.9.3", "@supabase/realtime-js": "2.9.4",
"@supabase/storage-js": "2.5.5" "@supabase/storage-js": "2.5.5"
} }
}, },
@ -8260,9 +8249,9 @@
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q=="
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.2.76", "version": "18.2.77",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.76.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.77.tgz",
"integrity": "sha512-T6z/v7YxpswDM61Vq5KoSPTJqCkroJfsDIsvXCr4+qOY6gik5Ju4w0jf67cpC5z7ydOnp/E0V0W08pDRy8u9Xw==", "integrity": "sha512-CUT9KUUF+HytDM7WiXKLF9qUSg4tGImwy4FXTlfEDPEkkNUzJ7rVFolYweJ9fS1ljoIaP7M7Rdjc5eUm/Yu5AA==",
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -9660,7 +9649,8 @@
"node_modules/boolbase": { "node_modules/boolbase": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "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": { "node_modules/bplist-creator": {
"version": "0.1.0", "version": "0.1.0",
@ -9947,9 +9937,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001608", "version": "1.0.30001609",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001608.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001609.tgz",
"integrity": "sha512-cjUJTQkk9fQlJR2s4HMuPMvTiRggl0rAVMtthQuyOlDWuqHXqN8azLq+pi8B2TjwKJ32diHjUqRIKeFX4z1FoA==", "integrity": "sha512-JFPQs34lHKx1B5t1EpQpWH4c+29zIyn/haGsbpfq3suuV9v56enjFt23zqijxGTMwy1p/4H2tjnQMY+p1WoAyA==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -10693,6 +10683,7 @@
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
"peer": true,
"dependencies": { "dependencies": {
"boolbase": "^1.0.0", "boolbase": "^1.0.0",
"css-what": "^6.1.0", "css-what": "^6.1.0",
@ -10708,6 +10699,7 @@
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"peer": true,
"dependencies": { "dependencies": {
"mdn-data": "2.0.14", "mdn-data": "2.0.14",
"source-map": "^0.6.1" "source-map": "^0.6.1"
@ -10720,6 +10712,7 @@
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -10728,6 +10721,7 @@
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
"peer": true,
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
}, },
@ -11273,6 +11267,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"peer": true,
"dependencies": { "dependencies": {
"domelementtype": "^2.3.0", "domelementtype": "^2.3.0",
"domhandler": "^5.0.2", "domhandler": "^5.0.2",
@ -11291,7 +11286,8 @@
"type": "github", "type": "github",
"url": "https://github.com/sponsors/fb55" "url": "https://github.com/sponsors/fb55"
} }
] ],
"peer": true
}, },
"node_modules/domexception": { "node_modules/domexception": {
"version": "4.0.0", "version": "4.0.0",
@ -11310,6 +11306,7 @@
"version": "5.0.3", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"peer": true,
"dependencies": { "dependencies": {
"domelementtype": "^2.3.0" "domelementtype": "^2.3.0"
}, },
@ -11324,6 +11321,7 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
"peer": true,
"dependencies": { "dependencies": {
"dom-serializer": "^2.0.0", "dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0", "domelementtype": "^2.3.0",
@ -11376,9 +11374,9 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.733", "version": "1.4.735",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.733.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.735.tgz",
"integrity": "sha512-gUI9nhI2iBGF0OaYYLKOaOtliFMl+Bt1rY7VmEjwxOxqoYLub/D9xmduPEhbw2imE6gYkJKhIE5it+KE2ulVxQ==" "integrity": "sha512-pkYpvwg8VyOTQAeBqZ7jsmpCjko1Qc6We1ZtZCjRyYbT5v4AIUKDy5cQTRotQlSSZmMr8jqpEt6JtOj5k7lR7A=="
}, },
"node_modules/emittery": { "node_modules/emittery": {
"version": "0.13.1", "version": "0.13.1",
@ -18140,7 +18138,8 @@
"node_modules/mdn-data": { "node_modules/mdn-data": {
"version": "2.0.14", "version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", "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": { "node_modules/memoize-one": {
"version": "5.2.1", "version": "5.2.1",
@ -19231,6 +19230,7 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"peer": true,
"dependencies": { "dependencies": {
"boolbase": "^1.0.0" "boolbase": "^1.0.0"
}, },
@ -20329,9 +20329,9 @@
} }
}, },
"node_modules/react-hook-form": { "node_modules/react-hook-form": {
"version": "7.51.2", "version": "7.51.3",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.2.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz",
"integrity": "sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==", "integrity": "sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==",
"engines": { "engines": {
"node": ">=12.22.0" "node": ">=12.22.0"
}, },
@ -20556,6 +20556,7 @@
"version": "15.1.0", "version": "15.1.0",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.1.0.tgz", "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.1.0.tgz",
"integrity": "sha512-p0Sx0EpQNk1nu6UcMEiB8K9P04n3J7s+pNYUwf1d/Yz+v4hk961VjuVqjyndgiEbHZyWiKWLZRVNuvLpwjPY2A==", "integrity": "sha512-p0Sx0EpQNk1nu6UcMEiB8K9P04n3J7s+pNYUwf1d/Yz+v4hk961VjuVqjyndgiEbHZyWiKWLZRVNuvLpwjPY2A==",
"peer": true,
"dependencies": { "dependencies": {
"css-select": "^5.1.0", "css-select": "^5.1.0",
"css-tree": "^1.1.3" "css-tree": "^1.1.3"

View File

@ -20,14 +20,13 @@
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "14.0.0", "@expo/vector-icons": "14.0.0",
"@fortawesome/fontawesome-free": "6.5.2",
"@fortawesome/fontawesome-svg-core": "6.5.2", "@fortawesome/fontawesome-svg-core": "6.5.2",
"@fortawesome/free-solid-svg-icons": "6.5.2", "@fortawesome/free-solid-svg-icons": "6.5.2",
"@fortawesome/react-native-fontawesome": "0.3.0", "@fortawesome/react-native-fontawesome": "0.3.0",
"@hookform/resolvers": "3.3.4", "@hookform/resolvers": "3.3.4",
"@react-native-async-storage/async-storage": "1.21.0", "@react-native-async-storage/async-storage": "1.21.0",
"@react-navigation/native": "6.1.16", "@react-navigation/native": "6.1.16",
"@supabase/supabase-js": "2.42.1", "@supabase/supabase-js": "2.42.3",
"expo": "50.0.15", "expo": "50.0.15",
"expo-font": "11.10.3", "expo-font": "11.10.3",
"expo-linking": "6.2.2", "expo-linking": "6.2.2",
@ -40,7 +39,7 @@
"lottie-react-native": "6.5.1", "lottie-react-native": "6.5.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "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": "0.73.6",
"react-native-calendars": "1.1304.1", "react-native-calendars": "1.1304.1",
"react-native-elements": "3.4.3", "react-native-elements": "3.4.3",
@ -49,7 +48,6 @@
"react-native-reanimated": "3.6.3", "react-native-reanimated": "3.6.3",
"react-native-safe-area-context": "4.8.2", "react-native-safe-area-context": "4.8.2",
"react-native-screens": "3.29.0", "react-native-screens": "3.29.0",
"react-native-svg": "15.1.0",
"react-native-url-polyfill": "2.0.0", "react-native-url-polyfill": "2.0.0",
"react-native-vector-icons": "10.0.3", "react-native-vector-icons": "10.0.3",
"react-native-web": "0.19.10", "react-native-web": "0.19.10",
@ -65,7 +63,7 @@
"@tsconfig/strictest": "2.0.5", "@tsconfig/strictest": "2.0.5",
"@types/jest": "29.5.12", "@types/jest": "29.5.12",
"@types/node": "20.12.7", "@types/node": "20.12.7",
"@types/react": "18.2.76", "@types/react": "18.2.77",
"@types/react-test-renderer": "18.0.7", "@types/react-test-renderer": "18.0.7",
"@typescript-eslint/eslint-plugin": "7.6.0", "@typescript-eslint/eslint-plugin": "7.6.0",
"@typescript-eslint/parser": "7.6.0", "@typescript-eslint/parser": "7.6.0",

View File

@ -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<HabitIconListProps> = (props) => {
const { selectedIcon, possibleIcons, handleIconSelect } = props
if (possibleIcons.length > 0) {
return (
<View>
{possibleIcons.map((icon) => {
return (
<IconButton
key={icon}
containerColor="white"
icon={({ size }) => {
return (
<FontAwesomeIcon
icon={icon as IconName}
size={size}
color={selectedIcon === icon ? "blue" : "black"}
/>
)
}}
size={30}
onPress={() => {
handleIconSelect(icon)
}}
/>
)
})}
</View>
)
}
return (
<View style={styles.noResults}>
<Text>No results found</Text>
</View>
)
}
const styles = StyleSheet.create({
noResults: {
marginTop: 20,
alignItems: "center",
},
})
export const HabitIconList = memo(HabitIconListWithoutMemo)

View File

@ -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<SearchInputProps> = (props) => {
const { searchText, handleSearch } = props
return (
<TextInput label="Search" value={searchText} onChangeText={handleSearch} />
)
}
const SearchInput = memo(SearchInputWithoutMemo)
export const HabitIconSelectorModal: React.FC<HabitIconSelectorModalProps> = ({
visible = false,
value,
onDismiss,
onSelect,
onPress,
}) => {
const [selectedIcon, setSelectedIcon] = useState<string>()
const [possibleIcons, setPossibleIcons] = useState<string[]>([])
const [searchText, setSearchText] = useState<string>(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 (
<>
<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}>
<SearchInput
searchText={searchText}
handleSearch={handleSearch}
/>
<HabitIconList
selectedIcon={selectedIcon}
possibleIcons={possibleIcons}
handleIconSelect={handleIconSelect}
/>
</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",
},
})

View File

@ -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 { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react"
import { Controller, useForm } from "react-hook-form" import { Controller, useForm } from "react-hook-form"
import { ScrollView, StyleSheet } from "react-native" import { ScrollView, StyleSheet, View } from "react-native"
import { import {
Button, Button,
HelperText, HelperText,
IconButton,
SegmentedButtons, SegmentedButtons,
Snackbar, Snackbar,
Text, Text,
TextInput, TextInput,
Tooltip,
} from "react-native-paper" } from "react-native-paper"
import { SafeAreaView } from "react-native-safe-area-context" import { SafeAreaView } from "react-native-safe-area-context"
import ColorPicker, { import ColorPicker, {
@ -17,7 +18,6 @@ import ColorPicker, {
Panel1, Panel1,
Preview, Preview,
} from "reanimated-color-picker" } from "reanimated-color-picker"
import { useState } from "react"
import type { GoalFrequency, GoalType } from "@/domain/entities/Goal" import type { GoalFrequency, GoalType } from "@/domain/entities/Goal"
import { GOAL_FREQUENCIES, GOAL_TYPES } 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 { HabitCreateSchema } from "@/domain/entities/Habit"
import type { User } from "@/domain/entities/User" import type { User } from "@/domain/entities/User"
import { useHabitsTracker } from "../../contexts/HabitsTracker" import { useHabitsTracker } from "../../contexts/HabitsTracker"
import { HabitIconSelectorModal } from "./HabitIconSelectorModal" import { useBoolean } from "../../hooks/useBoolean"
import { IconSelectorModal } from "./IconSelectorModal"
export interface HabitCreateFormProps { export interface HabitCreateFormProps {
user: User user: User
@ -58,6 +59,12 @@ export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
const [isVisibleSnackbar, setIsVisibleSnackbar] = useState(false) const [isVisibleSnackbar, setIsVisibleSnackbar] = useState(false)
const {
value: isModalIconSelectorVisible,
setTrue: openModalIconSelector,
setFalse: closeModalIconSelector,
} = useBoolean()
const onDismissSnackbar = (): void => { const onDismissSnackbar = (): void => {
setIsVisibleSnackbar(false) setIsVisibleSnackbar(false)
} }
@ -65,6 +72,7 @@ export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
const onSubmit = async (data: HabitCreateData): Promise<void> => { const onSubmit = async (data: HabitCreateData): Promise<void> => {
await habitsTrackerPresenter.habitCreate(data) await habitsTrackerPresenter.habitCreate(data)
setIsVisibleSnackbar(true) setIsVisibleSnackbar(true)
closeModalIconSelector()
reset() reset()
} }
@ -174,7 +182,7 @@ export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
]} ]}
> >
Habit Type Habit Type
<Tooltip {/* <Tooltip
title="Routine habits are activities performed regularly, while Target habits involve setting specific objectives to be achieved through repeated actions." title="Routine habits are activities performed regularly, while Target habits involve setting specific objectives to be achieved through repeated actions."
enterTouchDelay={50} enterTouchDelay={50}
leaveTouchDelay={25} leaveTouchDelay={25}
@ -186,7 +194,7 @@ export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
onPress={() => {}} onPress={() => {}}
style={{ alignSelf: "center" }} style={{ alignSelf: "center" }}
/> />
</Tooltip> </Tooltip> */}
</Text> </Text>
<SegmentedButtons <SegmentedButtons
style={[{ width: "90%" }]} style={[{ width: "90%" }]}
@ -230,13 +238,28 @@ export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
control={control} control={control}
render={({ field: { onChange, value } }) => { render={({ field: { onChange, value } }) => {
return ( return (
<HabitIconSelectorModal <View
visible style={{
value={value} justifyContent: "center",
onDismiss={() => {}} alignItems: "center",
onSelect={onChange} flexDirection: "row",
onPress={() => {}} gap: 20,
marginVertical: 30,
}}
>
<FontAwesomeIcon size={36} icon={value as IconName} />
<Button mode="contained" onPress={openModalIconSelector}>
Choose an icon
</Button>
<IconSelectorModal
key={isModalIconSelectorVisible ? "visible" : "hidden"}
isVisible={isModalIconSelectorVisible}
selectedIcon={value}
handleCloseModal={closeModalIconSelector}
onIconSelect={onChange}
/> />
</View>
) )
}} }}
name="icon" name="icon"
@ -247,7 +270,7 @@ export const HabitCreateForm: React.FC<HabitCreateFormProps> = ({ user }) => {
onPress={handleSubmit(onSubmit)} onPress={handleSubmit(onSubmit)}
loading={habitCreate.state === "loading"} loading={habitCreate.state === "loading"}
disabled={habitCreate.state === "loading"} disabled={habitCreate.state === "loading"}
style={[styles.spacing, { width: "90%" }]} style={[styles.spacing, { width: "100%" }]}
> >
Create your habit! 🚀 Create your habit! 🚀
</Button> </Button>

View File

@ -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 { zodResolver } from "@hookform/resolvers/zod"
import { useState } from "react" import { useState } from "react"
import { Controller, useForm } from "react-hook-form" 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 { Button, HelperText, Snackbar, TextInput } from "react-native-paper"
import { SafeAreaView } from "react-native-safe-area-context" import { SafeAreaView } from "react-native-safe-area-context"
import ColorPicker, { import ColorPicker, {
@ -13,6 +15,8 @@ import ColorPicker, {
import type { Habit, HabitEditData } from "@/domain/entities/Habit" import type { Habit, HabitEditData } from "@/domain/entities/Habit"
import { HabitEditSchema } from "@/domain/entities/Habit" import { HabitEditSchema } from "@/domain/entities/Habit"
import { useHabitsTracker } from "../../contexts/HabitsTracker" import { useHabitsTracker } from "../../contexts/HabitsTracker"
import { useBoolean } from "../../hooks/useBoolean"
import { IconSelectorModal } from "./IconSelectorModal"
export interface HabitEditFormProps { export interface HabitEditFormProps {
habit: Habit habit: Habit
@ -37,6 +41,12 @@ export const HabitEditForm: React.FC<HabitEditFormProps> = ({ habit }) => {
}, },
}) })
const {
value: isModalIconSelectorVisible,
setTrue: openModalIconSelector,
setFalse: closeModalIconSelector,
} = useBoolean()
const [isVisibleSnackbar, setIsVisibleSnackbar] = useState(false) const [isVisibleSnackbar, setIsVisibleSnackbar] = useState(false)
const onDismissSnackbar = (): void => { const onDismissSnackbar = (): void => {
@ -110,16 +120,30 @@ export const HabitEditForm: React.FC<HabitEditFormProps> = ({ habit }) => {
<Controller <Controller
control={control} control={control}
render={({ field: { onChange, onBlur, value } }) => { render={({ field: { onChange, value } }) => {
return ( return (
<TextInput <View
placeholder="Icon" style={{
onBlur={onBlur} justifyContent: "center",
onChangeText={onChange} alignItems: "center",
value={value} flexDirection: "row",
style={[styles.spacing, { width: "90%" }]} gap: 20,
mode="outlined" marginVertical: 30,
}}
>
<FontAwesomeIcon size={36} icon={value as IconName} />
<Button mode="contained" onPress={openModalIconSelector}>
Choose an icon
</Button>
<IconSelectorModal
key={isModalIconSelectorVisible ? "visible" : "hidden"}
isVisible={isModalIconSelectorVisible}
selectedIcon={value}
handleCloseModal={closeModalIconSelector}
onIconSelect={onChange}
/> />
</View>
) )
}} }}
name="icon" name="icon"

View File

@ -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<SearchInputProps> = (props) => {
const { searchText, handleSearch } = props
return (
<TextInput label="Search" value={searchText} onChangeText={handleSearch} />
)
}
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<IconSelectorModalProps> = ({
isVisible = false,
selectedIcon,
onIconSelect,
handleCloseModal,
}) => {
const [possibleIcons, setPossibleIcons] = useState<string[]>([])
const [isLoading, setIsLoading] = useState<boolean>(true)
const [searchText, setSearchText] = useState<string>("")
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 (
<Modal animationType="fade" transparent visible={isVisible}>
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<View
style={{
paddingHorizontal: 20,
paddingVertical: 5,
width: "96%",
height: "99%",
backgroundColor: "white",
borderColor: "black",
borderWidth: 1,
}}
>
<View
style={{
justifyContent: "center",
alignItems: "center",
marginVertical: 20,
}}
>
<Text style={{ marginVertical: 8 }}>Selected Icon:</Text>
<FontAwesomeIcon size={46} icon={selectedIcon as IconName} />
</View>
<SearchInput searchText={searchText} handleSearch={handleSearch} />
<ScrollView>
<List.Section title="Choose an icon:">
<IconsList
isLoading={isLoading}
selectedIcon={selectedIcon}
possibleIcons={possibleIcons}
handleIconSelect={handleIconSelect}
/>
</List.Section>
</ScrollView>
<View style={{ marginVertical: 15 }}>
<Button mode="contained" onPress={handleCloseModal}>
Save
</Button>
</View>
</View>
</View>
</Modal>
)
}

View File

@ -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<IconsListProps> = (props) => {
const {
selectedIcon,
possibleIcons,
isLoading = false,
handleIconSelect,
} = props
if (possibleIcons.length <= 0) {
return (
<View
style={{
marginTop: 20,
alignItems: "center",
}}
>
{isLoading ? (
<ActivityIndicator size="large" />
) : (
<Text>No results found</Text>
)}
</View>
)
}
return (
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
gap: 15,
justifyContent: "center",
alignItems: "center",
}}
>
{possibleIcons.map((icon) => {
return (
<IconButton
key={icon}
containerColor="white"
icon={({ size }) => {
return (
<FontAwesomeIcon
icon={icon as IconName}
size={size}
color={selectedIcon === icon ? "blue" : "black"}
/>
)
}}
size={30}
onPress={() => {
handleIconSelect(icon)
}}
/>
)
})}
</View>
)
}
export const IconsList = memo(IconsListWithoutMemo)

View File

@ -1,15 +1,16 @@
import FontAwesome6 from "@expo/vector-icons/FontAwesome6" import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"
import { useRouter } from "expo-router" import { useRouter } from "expo-router"
import { useState } from "react" import { useState } from "react"
import { View } from "react-native" import { View } from "react-native"
import { Checkbox, List, Text } from "react-native-paper" import { Checkbox, List, Text } from "react-native-paper"
import type LottieView from "lottie-react-native" import type LottieView from "lottie-react-native"
import type { IconName } from "@fortawesome/free-solid-svg-icons"
import type { GoalBoolean } from "@/domain/entities/Goal" import type { GoalBoolean } from "@/domain/entities/Goal"
import { GoalBooleanProgress } from "@/domain/entities/Goal" import { GoalBooleanProgress } from "@/domain/entities/Goal"
import type { HabitHistory } from "@/domain/entities/HabitHistory" import type { HabitHistory } from "@/domain/entities/HabitHistory"
import { getColorRGBAFromHex } from "@/utils/colors" import { getColorRGBAFromHex } from "@/utils/colors"
import { useHabitsTracker } from "../../contexts/HabitsTracker" import { useHabitsTracker } from "@/presentation/react/contexts/HabitsTracker"
export interface HabitCardProps { export interface HabitCardProps {
habitHistory: HabitHistory habitHistory: HabitHistory
@ -65,9 +66,9 @@ export const HabitCard: React.FC<HabitCardProps> = (props) => {
left={() => { left={() => {
return ( return (
<View style={{ justifyContent: "center", alignItems: "center" }}> <View style={{ justifyContent: "center", alignItems: "center" }}>
<FontAwesome6 <FontAwesomeIcon
size={24} size={24}
name={habit.icon} icon={habit.icon as IconName}
style={[ style={[
{ {
width: 30, width: 30,