diff --git a/.eslintrc.json b/.eslintrc.json index ac80492..4e54e11 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -52,7 +52,7 @@ "no-console": "warn", "react/jsx-filename-extension": [ "warn", - {"extensions": [".js", ".jsx", ".ts", ".tsx"]} + { "extensions": [".js", ".jsx", ".ts", ".tsx"] } ], "react/jsx-props-no-spreading": "off", "react-native/no-unused-styles": "error", @@ -106,7 +106,7 @@ "unicorn/empty-brace-spaces": "off", "unicorn/expiring-todo-comments": "off", "unicorn/no-array-for-each": "off", - "unicorn/filename-case": ["error", {"case": "camelCase"}], + "unicorn/filename-case": ["error", { "case": "camelCase" }], "unicorn/numeric-separators-style": "off" }, "ignorePatterns": ["*.config.js"] diff --git a/package-lock.json b/package-lock.json index f596e13..9d582b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "react-native-safe-area-context": "^4.6.4", "react-native-scoped-storage": "^1.9.3", "react-native-screens": "^3.22.1", - "react-native-share": "^9.2.2", + "react-native-share": "^9.2.3", "react-native-vector-icons": "^9.2.0", "react-native-video": "^5.2.1", "react-redux": "^8.1.1", @@ -13500,9 +13500,9 @@ } }, "node_modules/react-native-share": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-9.2.2.tgz", - "integrity": "sha512-z99dvVuSvVfQ7VwrO+JWdfurAgDpkMvdZYmqIDG9HX8RHXIqU6oNVx2ePuNWXn4UgySyFjMUKjGv99njM0934Q==" + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-9.2.3.tgz", + "integrity": "sha512-y6ju4HS6ydJoPVoacZ/Hp3i47AfI9W4e76Jv00r01dVbr6SCCcuqk37kIbn+kYivdTxOW77UGEbhtBHHtXnhzg==" }, "node_modules/react-native-vector-icons": { "version": "9.2.0", @@ -25846,9 +25846,9 @@ } }, "react-native-share": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-9.2.2.tgz", - "integrity": "sha512-z99dvVuSvVfQ7VwrO+JWdfurAgDpkMvdZYmqIDG9HX8RHXIqU6oNVx2ePuNWXn4UgySyFjMUKjGv99njM0934Q==" + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-9.2.3.tgz", + "integrity": "sha512-y6ju4HS6ydJoPVoacZ/Hp3i47AfI9W4e76Jv00r01dVbr6SCCcuqk37kIbn+kYivdTxOW77UGEbhtBHHtXnhzg==" }, "react-native-vector-icons": { "version": "9.2.0", diff --git a/package.json b/package.json index e09d771..ba9fb18 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "react-native-safe-area-context": "^4.6.4", "react-native-scoped-storage": "^1.9.3", "react-native-screens": "^3.22.1", - "react-native-share": "^9.2.2", + "react-native-share": "^9.2.3", "react-native-vector-icons": "^9.2.0", "react-native-video": "^5.2.1", "react-redux": "^8.1.1", diff --git a/src/database/meme.ts b/src/database/meme.ts index 6a176e0..44870cb 100644 --- a/src/database/meme.ts +++ b/src/database/meme.ts @@ -28,6 +28,7 @@ class Meme extends Realm.Object { description?: string; isFavorite!: boolean; tags!: Realm.List; + tagsLength!: number; dateCreated!: Date; dateModified!: Date; dateUsed?: Date; @@ -45,6 +46,7 @@ class Meme extends Realm.Object { description: 'string?', isFavorite: { type: 'bool', indexed: true, default: false }, tags: 'Tag[]', + tagsLength: { type: 'int', default: 0 }, dateCreated: { type: 'date', default: new Date() }, dateModified: { type: 'date', default: new Date() }, dateUsed: 'date?', diff --git a/src/database/tag.ts b/src/database/tag.ts index 0181271..c3e7090 100644 --- a/src/database/tag.ts +++ b/src/database/tag.ts @@ -6,9 +6,9 @@ class Tag extends Realm.Object { name!: string; color!: string; memes!: Realm.List; + memesLength!: number; dateCreated!: Date; dateModified!: Date; - dateUsed?: Date; timesUsed!: number; static schema: Realm.ObjectSchema = { @@ -19,9 +19,9 @@ class Tag extends Realm.Object { name: 'string', color: 'string', memes: 'Meme[]', + memesLength: { type: 'int', default: 0 }, dateCreated: { type: 'date', default: new Date() }, dateModified: { type: 'date', default: new Date() }, - dateUsed: 'date?', timesUsed: { type: 'int', default: 0 }, }, }; diff --git a/src/screens/home.tsx b/src/screens/home.tsx index b25b03e..93e52e6 100644 --- a/src/screens/home.tsx +++ b/src/screens/home.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { Button, Menu, @@ -7,22 +7,33 @@ import { Divider, useTheme, Searchbar, + HelperText, } from 'react-native-paper'; import { useDispatch, useSelector } from 'react-redux'; import { RootScrollView } from '../components'; import styles from '../styles'; -import { SORT, SORT_DIRECTION } from '../types'; +import { HOME_SORT, SORT_DIRECTION } from '../types'; import { getSortIcon, getViewIcon } from '../utilities'; import { RootState, - cycleView, - toggleSortDirection, - setSortDirection, - toggleFavoritesOnly, - setSort, - setFilter, + cycleHomeView, + toggleHomeSortDirection, + setHomeSortDirection, + toggleHomeFavoritesOnly, + setHomeSort, + setHomeFilter, } from '../state'; -import { MEME_TYPE, memeTypePlural } from '../database'; +import { MEME_TYPE, Meme, memeTypePlural } from '../database'; +import { useQuery } from '@realm/react'; + +const homeStyles = StyleSheet.create({ + headerButtonView: { + height: 50, + }, + helperText: { + marginVertical: 10, + }, +}); const Home = () => { const theme = useTheme(); @@ -40,26 +51,27 @@ const Home = () => { const [sortMenuVisible, setSortMenuVisible] = useState(false); const [filterMenuVisible, setFilterMenuVisible] = useState(false); - const handleSortModeChange = (newSort: SORT) => { + const handleSortModeChange = (newSort: HOME_SORT) => { if (newSort === sort) { - dispatch(toggleSortDirection()); + dispatch(toggleHomeSortDirection()); } else { - dispatch(setSort(newSort)); - if (newSort === SORT.TITLE) { - dispatch(setSortDirection(SORT_DIRECTION.ASCENDING)); + dispatch(setHomeSort(newSort)); + if (newSort === HOME_SORT.TITLE) { + dispatch(setHomeSortDirection(SORT_DIRECTION.ASCENDING)); } else { - dispatch(setSortDirection(SORT_DIRECTION.DESCENDING)); + dispatch(setHomeSortDirection(SORT_DIRECTION.DESCENDING)); } } setSortMenuVisible(false); }; const handleFilterChange = (newFilter: MEME_TYPE | undefined) => { - dispatch(setFilter(newFilter)); + dispatch(setHomeFilter(newFilter)); setFilterMenuVisible(false); }; const [search, setSearch] = useState(''); + const memes = useQuery('Meme'); return ( @@ -68,7 +80,12 @@ const Home = () => { value={search} onChangeText={setSearch} /> - + { Sort By: {sort} }> - {Object.keys(SORT).map(key => { + {Object.keys(HOME_SORT).map(key => { return ( - handleSortModeChange(SORT[key as keyof typeof SORT]) + handleSortModeChange( + HOME_SORT[key as keyof typeof HOME_SORT], + ) } - title={SORT[key as keyof typeof SORT]} + title={HOME_SORT[key as keyof typeof HOME_SORT]} /> ); })} - + dispatch(cycleView())} + size={16} + onPress={() => dispatch(cycleHomeView())} /> dispatch(toggleFavoritesOnly())} + size={16} + onPress={() => dispatch(toggleHomeFavoritesOnly())} /> { onPress={() => setFilterMenuVisible(true)} icon={filter ? 'filter' : 'filter-outline'} iconColor={theme.colors.primary} - size={18} + size={16} /> }> { + {/* TODO: Meme Views */} + {memes.length === 0 && ( + + + No memes found + + + )} ); }; diff --git a/src/screens/tags.tsx b/src/screens/tags.tsx index dd0e947..4141017 100644 --- a/src/screens/tags.tsx +++ b/src/screens/tags.tsx @@ -1,16 +1,35 @@ import React, { useState } from 'react'; -import { DataTable, HelperText, Searchbar } from 'react-native-paper'; +import { StyleSheet, View } from 'react-native'; +import { + Button, + DataTable, + Divider, + HelperText, + Menu, + Searchbar, +} from 'react-native-paper'; import { useQuery, useRealm } from '@realm/react'; +import { useDispatch, useSelector } from 'react-redux'; import { RootScrollView, TagChip } from '../components'; import { Tag, deleteTag } from '../database'; -import { StyleSheet, View } from 'react-native'; import styles from '../styles'; +import { + RootState, + setTagsSort, + setTagsSortDirection, + toggleTagsSortDirection, +} from '../state'; +import { SORT_DIRECTION, TAG_SORT, tagSortQuery } from '../types'; +import { getSortIcon } from '../utilities'; const tagStyles = StyleSheet.create({ + headerButtonView: { + height: 50, + }, helperText: { marginVertical: 10, }, - view: { + tagView: { justifyContent: 'center', maxWidth: '75%', }, @@ -18,37 +37,90 @@ const tagStyles = StyleSheet.create({ const Tags = () => { const realm = useRealm(); + const sort = useSelector((state: RootState) => state.tags.sort); + const sortDirection = useSelector( + (state: RootState) => state.tags.sortDirection, + ); + const dispatch = useDispatch(); + + const [sortMenuVisible, setSortMenuVisible] = useState(false); + + const handleSortModeChange = (newSort: TAG_SORT) => { + if (newSort === sort) { + dispatch(toggleTagsSortDirection()); + } else { + dispatch(setTagsSort(newSort)); + if (newSort === TAG_SORT.NAME) { + dispatch(setTagsSortDirection(SORT_DIRECTION.ASCENDING)); + } else { + dispatch(setTagsSortDirection(SORT_DIRECTION.DESCENDING)); + } + } + setSortMenuVisible(false); + }; const [search, setSearch] = useState(''); - const tags = useQuery('Tag').filtered(`name CONTAINS[c] "${search}"`); + const tags = useQuery('Tag') + .filtered(`name CONTAINS[c] "${search}"`) + .sorted(tagSortQuery(sort), sortDirection === SORT_DIRECTION.ASCENDING); return ( - + + + setSortMenuVisible(false)} + anchor={ + + }> + {Object.keys(TAG_SORT).map(key => { + return ( + + handleSortModeChange(TAG_SORT[key as keyof typeof TAG_SORT]) + } + title={TAG_SORT[key as keyof typeof TAG_SORT]} + /> + ); + })} + + + - - Tag - Items - {tags.map(tag => ( deleteTag(realm, tag)}> - + - {tag.memes.length} + {tag.memesLength} ))} {tags.length === 0 && ( - - No tags found - + + + No tags found + + )} ); diff --git a/src/state/home.ts b/src/state/home.ts index bf5188a..4b17baa 100644 --- a/src/state/home.ts +++ b/src/state/home.ts @@ -1,9 +1,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { SORT, SORT_DIRECTION, VIEW } from '../types'; +import { HOME_SORT, SORT_DIRECTION, VIEW } from '../types'; import { MEME_TYPE } from '../database'; interface HomeState { - sort: SORT; + sort: HOME_SORT; sortDirection: SORT_DIRECTION; view: VIEW; favoritesOnly: boolean; @@ -11,7 +11,7 @@ interface HomeState { } const initialState: HomeState = { - sort: SORT.TITLE, + sort: HOME_SORT.TITLE, sortDirection: SORT_DIRECTION.ASCENDING, view: VIEW.MASONRY, favoritesOnly: false, @@ -22,19 +22,19 @@ const homeSlice = createSlice({ name: 'home', initialState, reducers: { - setSort: (state, action: PayloadAction) => { + setHomeSort: (state, action: PayloadAction) => { state.sort = action.payload; }, - setSortDirection: (state, action: PayloadAction) => { + setHomeSortDirection: (state, action: PayloadAction) => { state.sortDirection = action.payload; }, - toggleSortDirection: state => { + toggleHomeSortDirection: state => { state.sortDirection ^= 1; }, - setView: (state, action: PayloadAction) => { + setHomeView: (state, action: PayloadAction) => { state.view = action.payload; }, - cycleView: state => { + cycleHomeView: state => { switch (state.view) { case VIEW.MASONRY: { state.view = VIEW.GRID; @@ -50,38 +50,38 @@ const homeSlice = createSlice({ } } }, - setFavoritesOnly: (state, action: PayloadAction) => { + setHomeFavoritesOnly: (state, action: PayloadAction) => { state.favoritesOnly = action.payload; }, - toggleFavoritesOnly: state => { + toggleHomeFavoritesOnly: state => { state.favoritesOnly = !state.favoritesOnly; }, - setFilter: (state, action: PayloadAction) => { + setHomeFilter: (state, action: PayloadAction) => { state.filter = action.payload; }, }, }); const { - setSort, - setSortDirection, - toggleSortDirection, - setView, - cycleView, - setFavoritesOnly, - toggleFavoritesOnly, - setFilter, + setHomeSort, + setHomeSortDirection, + toggleHomeSortDirection, + setHomeView, + cycleHomeView, + setHomeFavoritesOnly, + toggleHomeFavoritesOnly, + setHomeFilter, } = homeSlice.actions; export { type HomeState, - setSort, - setSortDirection, - toggleSortDirection, - setView, - cycleView, - setFavoritesOnly, - toggleFavoritesOnly, - setFilter, + setHomeSort, + setHomeSortDirection, + toggleHomeSortDirection, + setHomeView, + cycleHomeView, + setHomeFavoritesOnly, + toggleHomeFavoritesOnly, + setHomeFilter, }; export default homeSlice.reducer; diff --git a/src/state/index.ts b/src/state/index.ts index 18c75c0..0f28f7c 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -12,15 +12,18 @@ import { import { createRealmPersistStorage } from '@bankify/redux-persist-realm'; import settingsReducer from './settings'; import homeReducer from './home'; +import tagsReducer from './tags'; const rootReducer = combineReducers({ settings: settingsReducer, home: homeReducer, + tags: tagsReducer, }); interface RootState { settings: ReturnType; home: ReturnType; + tags: ReturnType; } const persistConfig = { @@ -51,12 +54,18 @@ export { } from './settings'; export { type HomeState, - setSort, - setSortDirection, - toggleSortDirection, - setView, - cycleView, - setFavoritesOnly, - toggleFavoritesOnly, - setFilter, + setHomeSort, + setHomeSortDirection, + toggleHomeSortDirection, + setHomeView, + cycleHomeView, + setHomeFavoritesOnly, + toggleHomeFavoritesOnly, + setHomeFilter, } from './home'; +export { + type TagsState, + setTagsSort, + setTagsSortDirection, + toggleTagsSortDirection, +} from './tags'; diff --git a/src/state/tags.ts b/src/state/tags.ts new file mode 100644 index 0000000..0bf90db --- /dev/null +++ b/src/state/tags.ts @@ -0,0 +1,39 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { TAG_SORT, SORT_DIRECTION } from '../types'; + +interface TagsState { + sort: TAG_SORT; + sortDirection: SORT_DIRECTION; +} + +const initialState: TagsState = { + sort: TAG_SORT.NAME, + sortDirection: SORT_DIRECTION.DESCENDING, +}; + +const tagsSlice = createSlice({ + name: 'tags', + initialState, + reducers: { + setTagsSort: (state, action: PayloadAction) => { + state.sort = action.payload; + }, + setTagsSortDirection: (state, action: PayloadAction) => { + state.sortDirection = action.payload; + }, + toggleTagsSortDirection: state => { + state.sortDirection ^= 1; + }, + }, +}); + +const { setTagsSort, setTagsSortDirection, toggleTagsSortDirection } = + tagsSlice.actions; + +export { + type TagsState, + setTagsSort, + setTagsSortDirection, + toggleTagsSortDirection, +}; +export default tagsSlice.reducer; diff --git a/src/types/index.ts b/src/types/index.ts index 8a8cbac..c27df43 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,2 +1,8 @@ -export { SORT, SORT_DIRECTION } from './sort'; +export { + HOME_SORT, + homeSortQuery, + TAG_SORT, + tagSortQuery, + SORT_DIRECTION, +} from './sort'; export { VIEW } from './view'; diff --git a/src/types/sort.ts b/src/types/sort.ts index 6118fea..0d67d00 100644 --- a/src/types/sort.ts +++ b/src/types/sort.ts @@ -1,4 +1,4 @@ -enum SORT { +enum HOME_SORT { TITLE = 'Title', DATE_CREATED = 'Date Created', DATE_MODIFIED = 'Date Modified', @@ -7,9 +7,60 @@ enum SORT { SIZE = 'Size', } +const homeSortQuery = (sort: HOME_SORT) => { + switch (sort) { + case HOME_SORT.TITLE: { + return 'title'; + } + case HOME_SORT.DATE_CREATED: { + return 'dateCreated'; + } + case HOME_SORT.DATE_MODIFIED: { + return 'dateModified'; + } + case HOME_SORT.DATE_USED: { + return 'dateUsed'; + } + case HOME_SORT.TIMES_USED: { + return 'timesUsed'; + } + case HOME_SORT.SIZE: { + return 'size'; + } + } +}; + +enum TAG_SORT { + NAME = 'Name', + MEMES_LENGTH = 'Items', + DATE_CREATED = 'Date Created', + DATE_MODIFIED = 'Date Modified', + TIMES_USED = 'Times Used', +} + +const tagSortQuery = (sort: TAG_SORT) => { + switch (sort) { + case TAG_SORT.NAME: { + return 'name'; + } + case TAG_SORT.MEMES_LENGTH: { + return 'memesLength'; + } + case TAG_SORT.DATE_CREATED: { + return 'dateCreated'; + } + case TAG_SORT.DATE_MODIFIED: { + return 'dateModified'; + } + case TAG_SORT.TIMES_USED: { + return 'timesUsed'; + } + } +}; + enum SORT_DIRECTION { ASCENDING = 0, DESCENDING = 1, } -export { SORT, SORT_DIRECTION }; +export { HOME_SORT, homeSortQuery, TAG_SORT, tagSortQuery, SORT_DIRECTION }; diff --git a/src/utilities/icon.ts b/src/utilities/icon.ts index e156cec..f2ad6ed 100644 --- a/src/utilities/icon.ts +++ b/src/utilities/icon.ts @@ -1,21 +1,29 @@ -import { SORT, SORT_DIRECTION, VIEW } from '../types'; +import { HOME_SORT, SORT_DIRECTION, TAG_SORT, VIEW } from '../types'; -const getSortIcon = (sort: SORT, sortDirection: SORT_DIRECTION) => { +const getSortIcon = ( + sort: HOME_SORT | TAG_SORT, + sortDirection: SORT_DIRECTION, +) => { let sortIcon = ''; switch (sort) { - case SORT.TITLE: { + case HOME_SORT.TITLE: + case TAG_SORT.NAME: { sortIcon = 'sort-alphabetical'; break; } - case SORT.DATE_CREATED: - case SORT.DATE_MODIFIED: - case SORT.DATE_USED: { + case HOME_SORT.DATE_CREATED: + case HOME_SORT.DATE_MODIFIED: + case HOME_SORT.DATE_USED: + case TAG_SORT.DATE_CREATED: + case TAG_SORT.DATE_MODIFIED: { sortIcon = 'sort-calendar'; break; } - case SORT.TIMES_USED: - case SORT.SIZE: { + case HOME_SORT.TIMES_USED: + case HOME_SORT.SIZE: + case TAG_SORT.MEMES_LENGTH: + case TAG_SORT.TIMES_USED: { sortIcon = 'sort-numeric'; break; }