diff --git a/APP_DEVELOPMENT.md b/APP_DEVELOPMENT.md index f1abbd25..6e9b6023 100644 --- a/APP_DEVELOPMENT.md +++ b/APP_DEVELOPMENT.md @@ -66,11 +66,11 @@ The app designs are produced in Figma. You can inspect different elements within Our app is built using React Native working with the [Expo CLI (command-line tool).](https://docs.expo.dev/more/expo-cli/) and [Expo Go.](https://expo.dev/client) -It's easy to be confused about React Native and Expo, partly because they have changed over time as has the relationship between them (it's easy to find articles which are out of date) and because there are different ways to use them. [This article](https://retool.com/blog/expo-cli-vs-react-native-cli/) has quite a good background. +It's easy to be confused about React Native and Expo, partly because they have changed over time as has the relationship between them (it's easy to find articles which are out of date) and because there are different ways to use them. [This article](https://retool.com/blog/expo-cli-vs-react-native-cli/) has quite a good background. ### Expo trade-offs -We are using the Expo CLI and Expo Go because they make development and deployment of the app a lot easier. The main trade-off is that we cannot use every npm package that works with React Native. +We are using the Expo CLI and Expo Go because they make development and deployment of the app a lot easier. The main trade-off is that we cannot use every npm package that works with React Native. There are many React Native packages we can use, but when it involves interacting with the device hardware (e.g. camera, GPS, etc) or the OS (e.g. the clipboard, file storage or sharing) or occasionally other areas (e.g. SVGs) then we are usually limited to packages that are supported as part of the Expo SDK library (but for the kind of app we're building, this should be ok). @@ -145,7 +145,7 @@ When you're building (or changing) a component or container, or changing a theme NativeBase does some handling of dark mode straight out of the box, so you may not need to change anything. -**If you're switching a container to use NativeBase** check out `SettingsContainer` and `VerticalStackContainer` examples as they're already working reasonably well with dark mode. One of the things you'll need to do in your container is switch from using the old theme and switch from using any `styled` components/views. +**If you're switching a container to use NativeBase** check out `SettingsContainer` example as they're already working reasonably well with dark mode. One of the things you'll need to do in your container is switch from using the old theme and switch from using any `styled` components/views. **If you need to set colours based on dark/light mode** [see the docs here](https://docs.nativebase.io/dark-mode) and wherever possible set `_light` and `_dark` properties in the `StaTheme` file (approach 1. in the docs) rather than setting them on your individual component -- i.e. try to make settings as universal and as easily reusable as possible. @@ -279,27 +279,27 @@ There are broadly two kinds of things that can go wrong on the front-end app: Ordinarily, when you are developing using Expo Go, you should **not** have `BUGSNAG_API_KEY` set to the real API key value, otherwise it will send crash logs to Bugsnag which we usually don't want (ordinarily we only want to use Bugsnag to track errors and crashes on real devices). - > You do need to set a value otherwise the app will not run, so you can use e.g. `BUGSNAG_API_KEY="no_api_key"` +> You do need to set a value otherwise the app will not run, so you can use e.g. `BUGSNAG_API_KEY="no_api_key"` - > During development, in the terminal window where you are running Expo you may see the message `[bugsnag] Bugsnag.start() was called more than once. Ignoring.` You don't need to worry about this, it's just the app reloading and trying to start Bugsnag again. +> During development, in the terminal window where you are running Expo you may see the message `[bugsnag] Bugsnag.start() was called more than once. Ignoring.` You don't need to worry about this, it's just the app reloading and trying to start Bugsnag again. ### Seeing Bugsnag reports Ask one of the team to add you to the **it470-volunteer-app-errors** Slack channel where you can see the latest bugs coming in from the app on real devices, and the API production server. -If you know a bug has been fixed or can safely be ignored, please click the 'Mark as fixed' or 'Ignore' button on the error in the Slack message. +If you know a bug has been fixed or can safely be ignored, please click the 'Mark as fixed' or 'Ignore' button on the error in the Slack message. -To get more details on a bug you'll need to go to [our Bugsnag inbox here](https://app.bugsnag.com/scottish-tech-army/volunteer-app/errors) -- you'll need the Bugsnag login details from another team member. +To get more details on a bug you'll need to go to [our Bugsnag inbox here](https://app.bugsnag.com/scottish-tech-army/volunteer-app/errors) -- you'll need the Bugsnag login details from another team member. - > Note: there are two Projects in Bugsnag -- one for the front-end app ('Volunteer app'), another for the API ('Volunteer app API'). Make sure you're looking at the right one. You can also filter by development/production. +> Note: there are two Projects in Bugsnag -- one for the front-end app ('Volunteer app'), another for the API ('Volunteer app API'). Make sure you're looking at the right one. You can also filter by development/production. - > When you look into an error, click on it in the Inbox in Bugsnag, then on the 'Stacktrace' tab you'll need to find where the error originated. The first entry in the stacktrace is just the `logging` module, you need to find what's below that and click to expand it to see where in the code the error actually occurred. +> When you look into an error, click on it in the Inbox in Bugsnag, then on the 'Stacktrace' tab you'll need to find where the error originated. The first entry in the stacktrace is just the `logging` module, you need to find what's below that and click to expand it to see where in the code the error actually occurred. ### Logging errors to Bugsnag during development -You can log errors to Bugsnag when developing in Expo Go if you really need to. **You shouldn't normally need to do this -- Bugsnag error logging is usually to monitor crashes and errors in the production app. You should only use this in development when normal error detection is insufficient** e.g. because you want to figure out why the app is crashing due to a lack of memory. **Don't** use this in place of normal code tools like `console.error` and `console.log` and other normal testing approaches. +You can log errors to Bugsnag when developing in Expo Go if you really need to. **You shouldn't normally need to do this -- Bugsnag error logging is usually to monitor crashes and errors in the production app. You should only use this in development when normal error detection is insufficient** e.g. because you want to figure out why the app is crashing due to a lack of memory. **Don't** use this in place of normal code tools like `console.error` and `console.log` and other normal testing approaches. -You can force the app to report errors to Bugsnag during development using Expo Go (this also overrides the user permissions setting which normally determines whether or not send error reports). To do this: +You can force the app to report errors to Bugsnag during development using Expo Go (this also overrides the user permissions setting which normally determines whether or not send error reports). To do this: - Create a `.env` file if you don't have one already, and add `BUGSNAG_API_KEY="xxxxxxxxxxxxxxxx"` repacing `xxxxxxxxxxxxxxxx` with the value of our actual Bugsnag API key for the app (ask on the team Slack channel and someone can give you this -- note: the front-end app and the API use different Bugsnag API keys) - In your `.env` file include the line `BUGSNAG_ALWAYS_SEND_BUGS="true"` (this forces the API to send errors to Bugsnag, even though you're in a development environment) diff --git a/src/Components/Project/ProjectFilterSort.tsx b/src/Components/Project/ProjectFilterSort.tsx deleted file mode 100644 index b906a812..00000000 --- a/src/Components/Project/ProjectFilterSort.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @file Project Filter Sort - */ - -import React from 'react' -import styled from 'styled-components/native' -import underDevelopmentAlert from '../../Utils/UnderDevelopmentAlert' - -const FilterSortView = styled.View` - display: flex; - flex-direction: row; - justify-content: space-around; - margin: 23px 10px; -` -const FilterSortText = styled.Text` - font-size: 18px; - text-decoration: underline; -` -const FilterSortTouch = styled.TouchableOpacity`` - -const ProjectFilterSort = () => { - return ( - - - Filter - - - Sort - - - ) -} - -export default ProjectFilterSort diff --git a/src/NativeBase/Components/FreeSearchBar.tsx b/src/NativeBase/Components/FreeSearchBar.tsx index cc8fcef9..f9fad335 100644 --- a/src/NativeBase/Components/FreeSearchBar.tsx +++ b/src/NativeBase/Components/FreeSearchBar.tsx @@ -2,13 +2,15 @@ * @file Text input for searching. */ -import { Box, Input } from 'native-base' +import { Box, Icon, IconButton, Input } from 'native-base' import React, { FC, useState } from 'react' +import MaterialIcons from 'react-native-vector-icons/MaterialIcons' import SearchIconHighlighted from './SearchIconHighlighted' export interface FreeSearchBarProps { handleChangeText?: (updatedText: string) => void handleSubmit: (submitText: string) => void + handleClearSearch?: () => void marginBottom?: string marginTop?: string } @@ -26,6 +28,7 @@ export interface FreeSearchBarProps { const FreeSearchBar: FC = ({ handleChangeText, handleSubmit, + handleClearSearch, marginBottom, marginTop, }) => { @@ -39,12 +42,34 @@ const FreeSearchBar: FC = ({ const onSubmitEditing = () => handleSubmit(text) + const clearText = () => { + setText('') + if (handleClearSearch) handleClearSearch() // Call the clear function passed as prop + } + return ( } + InputRightElement={ + text ? ( + + } + onPress={clearText} + variant="link" + _pressed={{ bg: 'gray.200' }} + /> + ) : undefined + } lineHeight="md" marginBottom={marginBottom ?? '4'} marginTop={marginTop ?? '0'} @@ -56,6 +81,14 @@ const FreeSearchBar: FC = ({ placeholder="Search..." textAlignVertical="center" value={text} + borderWidth={0} + backgroundColor={text ? 'gray.200' : 'gray.100'} + borderRadius={15} + placeholderTextColor="gray.400" + color="gray.800" + _focus={{ + borderColor: 'transparent', // Ensure no border color on focus + }} /> ) diff --git a/src/NativeBase/Components/ProjectTagButtonsFilter.tsx b/src/NativeBase/Components/ProjectTagButtonsFilter.tsx new file mode 100644 index 00000000..c589dc09 --- /dev/null +++ b/src/NativeBase/Components/ProjectTagButtonsFilter.tsx @@ -0,0 +1,139 @@ +import { + Projects, + ProjectSector, + ProjectsSearchField, + ProjectTechnology, +} from '@/Services/modules/projects' +import { RoleGroupName } from '@/Services/modules/projects/roleGroups' +import { ProjectsState } from '@/Store/Projects' +import { VStack } from 'native-base' +import React, { FC, useState } from 'react' +import { useSelector } from 'react-redux' +import { ListSearch } from '../Containers/ListContainer' +import ChoicesList, { + ChoicesListChoice, + ChoicesListColour, + ChoicesListFontStyle, +} from './ChoicesList' +import TagButtons from './TagButtons' + +export interface ProjectSearch extends ListSearch { + results: Projects // the projects results for this search +} +export interface ProjectsTagButtonsFilterProps { + handleQuickSearchSubmit: ( + searchField: ProjectsSearchField, + searchQueryChoice: string, + ) => void +} + +const ProjectsTagButtonsFilter: FC = ({ + handleQuickSearchSubmit, +}) => { + // Fetch all projects from the store + const allProjects = useSelector( + (state: { projects: ProjectsState }) => state.projects.projects, + ) + + // State to track the currently active tag (using simple strings) + const [activeTag, setActiveTag] = useState(null) + + // Define which quick search options to use + const quickSearchRoleGroupNames: RoleGroupName[] = [ + RoleGroupName.WebDeveloper, + RoleGroupName.TechSupport, + RoleGroupName.UIUX, + RoleGroupName.Researcher, + RoleGroupName.BAPM, + RoleGroupName.ScrumMaster, + ] + const quickSearchRoleChoices: ChoicesListChoice[] = + quickSearchRoleGroupNames.map( + roleGroupName => + ({ + text: roleGroupName, + onPress: () => + handleQuickSearchSubmit(ProjectsSearchField.Role, roleGroupName), + } as ChoicesListChoice), + ) + + const quickSearchTechnologies: ChoicesListChoice[] = Object.values( + ProjectTechnology, + ).map( + technology => + ({ + text: technology, + onPress: () => + handleQuickSearchSubmit(ProjectsSearchField.Skills, technology), + } as ChoicesListChoice), + ) + + const quickSearchCauses: ChoicesListChoice[] = Object.values( + ProjectSector, + ).map( + cause => + ({ + text: cause, + onPress: () => + handleQuickSearchSubmit(ProjectsSearchField.Sector, cause), + } as ChoicesListChoice), + ) + + // Define colour and style to use for quick search options + const quickSearchListColour = ChoicesListColour.primary + const quickSearchListStyle = ChoicesListFontStyle.mediumLight + + /** + * Handle tag press logic + * If the tag is already open, close it by setting activeTag to null. + * If a different tag is clicked, close the previous one and open the new one. + */ + const handleTagPress = (tag: string) => { + setActiveTag(activeTag === tag ? null : tag) + } + + return ( + + {/* Tag Buttons for Roles, Tech, and Causes */} + + + {/* Show the list of role choices if Roles tab is active */} + {activeTag === 'Roles' && ( + + )} + + {/* Show the list of technology choices if Tech tab is active */} + {activeTag === 'Tech' && ( + + )} + + {/* Show the list of causes if Causes tag is active */} + {activeTag === 'Causes' && ( + + )} + + + ) +} + +export default ProjectsTagButtonsFilter diff --git a/src/NativeBase/Components/SearchIconHighlighted.tsx b/src/NativeBase/Components/SearchIconHighlighted.tsx index 964794d6..4b32116f 100644 --- a/src/NativeBase/Components/SearchIconHighlighted.tsx +++ b/src/NativeBase/Components/SearchIconHighlighted.tsx @@ -12,13 +12,7 @@ import MaterialIcons from 'react-native-vector-icons/MaterialIcons' * @returns {React.ReactElement} Component */ const SearchIconHighlighted = () => ( - + ) export default SearchIconHighlighted diff --git a/src/NativeBase/Components/TagButtons.tsx b/src/NativeBase/Components/TagButtons.tsx new file mode 100644 index 00000000..6f130795 --- /dev/null +++ b/src/NativeBase/Components/TagButtons.tsx @@ -0,0 +1,56 @@ +import React from 'react' +import { HStack, Icon, IconButton, Pressable, Text } from 'native-base' +import MaterialIcons from 'react-native-vector-icons/MaterialIcons' + +interface TagButtonsProps { + tags: string[] // An array of tag names, e.g., ['Roles', 'Tech', 'Causes'] + iconState: Record // A dynamic record that maps tags to boolean states + handleTagPress: (tag: string) => void // A function that takes any tag +} + +const TagButtons: React.FC = ({ + tags, + iconState, + handleTagPress, +}) => { + return ( + + {tags.map(tag => ( + handleTagPress(tag)} + borderRadius={40} + pl={4} + pr={2} + py={2} + display="flex" + flexDirection="row" + alignItems="center" + justifyContent="center" + backgroundColor={iconState[tag] ? 'primary.20' : 'gray.100'} + borderColor={iconState[tag] ? 'primary.100' : 'transparent'} + borderWidth={1} + > + {tag} + + } + variant="outline" + _icon={{ color: 'gray.500' }} + onPress={() => handleTagPress(tag)} + /> + + ))} + + ) +} + +export default TagButtons diff --git a/src/NativeBase/Components/TopOfApp.tsx b/src/NativeBase/Components/TopOfApp.tsx index afd256f4..a540fb56 100644 --- a/src/NativeBase/Components/TopOfApp.tsx +++ b/src/NativeBase/Components/TopOfApp.tsx @@ -4,19 +4,17 @@ * Follows example here https://docs.nativebase.io/building-app-bar */ +import StaLogoWideDarkMode from '@/NativeBase/Assets/Images/Logos/sta-logo-wide-dark-mode.svg' +import StaLogoWide from '@/NativeBase/Assets/Images/Logos/sta-logo-wide.svg' import { Box, - HStack, - Icon, - IconButton, + Heading, StatusBar, useColorMode, useColorModeValue, + VStack, } from 'native-base' import React, { FC } from 'react' -import MaterialIcons from 'react-native-vector-icons/MaterialIcons' -import StaLogoWide from '@/NativeBase/Assets/Images/Logos/sta-logo-wide.svg' -import StaLogoWideDarkMode from '@/NativeBase/Assets/Images/Logos/sta-logo-wide-dark-mode.svg' import StaTheme from '../Theme/StaTheme' interface TopOfAppProps { @@ -63,7 +61,13 @@ const TopOfApp: FC = ({ _light={{ backgroundColor: StaTheme.colors.bg['100'] }} safeAreaTop > - + + Roles + + + + {/* = ({ /> )} - + */} ) diff --git a/src/NativeBase/Containers/ListContainer.tsx b/src/NativeBase/Containers/ListContainer.tsx index 090ea647..db78e647 100644 --- a/src/NativeBase/Containers/ListContainer.tsx +++ b/src/NativeBase/Containers/ListContainer.tsx @@ -7,29 +7,19 @@ /* eslint-disable @typescript-eslint/no-shadow */ -import { Heading, VStack } from 'native-base' -import React, { useEffect, useState } from 'react' -import { Dimensions } from 'react-native' -import { useDispatch, useSelector } from 'react-redux' -import styled from 'styled-components/native' -import { EventSearch } from '@/Containers/EventSearchContainer' -import { ProjectSearch } from './ProjectSearchContainer' import EventOptions from '@/Components/Event/EventOptions' import EventSearchUpcomingQuickSearch, { EventQuickSearchUpcomingChoice, } from '@/Components/Event/EventSearchQuickSearchUpcoming' +import { EventSearch } from '@/Containers/EventSearchContainer' +import FreeSearchBar from '@/NativeBase/Components/FreeSearchBar' import List, { ListDisplayMode, ListOptions, } from '@/NativeBase/Components/List' -import ProjectFilterSort from '@/Components/Project/ProjectFilterSort' +import SkeletonLoading from '@/NativeBase/Components/SkeletonLoading' import TopOfApp from '@/NativeBase/Components/TopOfApp' import { navigate, RootStackParamList } from '@/Navigators/utils' -import { heightOfTopOfAppPlusBottomNav } from '@/Utils/Layout' -import { capitaliseFirstLetter } from '@/Utils/Text' - -import { SegmentedPickerOption } from '../Components/SegmentedPicker' - import { Events, EventsRange, @@ -37,13 +27,28 @@ import { useLazyFetchAllUpcomingEventsQuery, } from '@/Services/modules/events' import { + Project, Projects, + ProjectsSearchField, useLazyFetchAllProjectsQuery, } from '@/Services/modules/projects' +import { roleGroups } from '@/Services/modules/projects/roleGroups' import { EventsState, setEvents } from '@/Store/Events' import { ProjectsState, setProjects } from '@/Store/Projects' +import { fuzzySearchByArray } from '@/Utils/Search' +import { capitaliseFirstLetter } from '@/Utils/Text' import underDevelopmentAlert from '@/Utils/UnderDevelopmentAlert' -import SkeletonLoading from '../Components/SkeletonLoading' +import Fuse from 'fuse.js' +import { HStack, Icon, IconButton, ScrollView, View, VStack } from 'native-base' +import React, { useEffect, useState } from 'react' +import { Dimensions } from 'react-native' +import MaterialIcons from 'react-native-vector-icons/MaterialIcons' +import { useDispatch, useSelector } from 'react-redux' +import styled from 'styled-components/native' +import ProjectsTagButtonsFilter, { + ProjectSearch, +} from '../Components/ProjectTagButtonsFilter' +import { SegmentedPickerOption } from '../Components/SegmentedPicker' const ClearSearchLabel = styled.Text` @@ -104,7 +109,9 @@ const ListContainer = (props: { // Shared const dispatch = useDispatch() const [listItemsToShow, setListItemsToShow] = useState() + const params = props.route.params + const screens = { list: { [ListType.Events]: 'Events' as keyof RootStackParamList, @@ -147,6 +154,21 @@ const ListContainer = (props: { const allProjects = useSelector( (state: { projects: ProjectsState }) => state.projects?.projects, ) + // const [searchText, setSearchText] = useState('') + + // State for toggling icons + const [iconState, setIconState] = useState>({ + Roles: false, + Tech: false, + Causes: false, + }) + + const handleTagPress = (tag: string) => { + setIconState(prevState => ({ + ...prevState, + [tag]: !prevState[tag], // Toggle the state + })) + } /* * @@ -225,6 +247,70 @@ const ListContainer = (props: { allProjects, ]) + // Ensure job title searches find related roles + const getRelatedRoles = ( + possibleRoleSearchQuery: string, + ): string[] | undefined => { + const fuse = new Fuse(roleGroups, { + keys: ['roleNames'], + minMatchCharLength: 2, + threshold: 0.1, + }) + + const fuseResults = fuse.search(possibleRoleSearchQuery) + + if (fuseResults.length) { + const roles = [] + + for (const fuseResult of fuseResults) { + for (const role of fuseResult.item.roleNames) { + roles.push(role) + } + } + + return roles + } + + return undefined + } + + const handleFreeTextSubmit = (freeTextSearchQuery: string) => { + // Add free text to list of search queries + const searchQueries = [freeTextSearchQuery] + + // If the free text query matches a group of job roles, add these to the list of search queries too + const relatedRoles = getRelatedRoles(freeTextSearchQuery) + if (relatedRoles?.length) { + searchQueries.push(...relatedRoles) + } + + const results = fuzzySearchByArray( + allProjects, + [ + { name: 'client', weight: 1 }, + { name: 'description', weight: 0.5 }, + { name: 'name', weight: 1 }, + { name: 'role', weight: 1 }, + { name: 'skills', weight: 1 }, + { name: 'sector', weight: 1 }, + ], + searchQueries, + ) + + const description = `"${freeTextSearchQuery}"` + + navigate( + 'Projects' as keyof RootStackParamList, + { + type: ListType.Projects, + search: { + results, + description, + } as ProjectSearch, + } as ListRouteParams, + ) + } + // Clear the search so the user's seeing all data instead const clearSearch = () => { if (params?.type) { @@ -269,77 +355,167 @@ const ListContainer = (props: { } }, [params?.options?.events, params?.search, params?.type]) + function updateListForTestWithRoleWebDeveloper(): void { + //TODO + // 1. when we click on the button, we want the list of projects to be filtered for roles category web-developer and displayed on the same screen. + /* + console.log(listItemsToShow!.map((item: Project) => item.role).join(', ')) + const filteredList: Project[] = listItemsToShow!.filter((item: Project) => item.role === 'User Researcher') + setListItemsToShow(filteredList) + */ + /* THIS DOES NOT WORK +const filteredList: Project[] = listItemsToShow!.filter((item: Project) => item.role === 'User Researcher') +params.search = { results: filteredList, description: 'fake filtering' } as ProjectSearch +*/ + + /* THIS WORKS TOO: calling directly the already partly implemented search functionality of the ListContainer component + */ + const searchResults: ProjectSearch = { + description: 'test description', + results: (listItemsToShow as Projects).filter( + (item: Project) => item.role === 'User Researcher', + ), + } + navigate(screens.list[params.type], { + type: params.type, + search: searchResults, + } as ListRouteParams) + + // 2. we want to use existing filtering logic to manipulate the list of projects and show it in the list + // 3. we are really cool and start using maybe redux selectors to apply the filtering (maybe?) + // 4. step 3 is OPTIONAL. once we have managed to get to step 2, we will now focus on doing roles filtering using our tagbuttons. + // 5. we will extend this logic also to the tech and fields area + // 6. we will now focus on the fullsearch bar - maybe break down in additional sub tasks to make progress easier... + // 7. we clean up unused components + // 8. we start refactoring and making the code/architecture more clean + } + + // const onSubmitEditing = (text: string) => text + + const handleQuickSearchSubmit = ( + searchField: ProjectsSearchField, + searchQueryChoice: string, + ) => { + let searchQueries = [searchQueryChoice] + let results: Projects = [] + + // Add related roles if the search is by role + if (searchField === ProjectsSearchField.Role) { + const relatedRoles = getRelatedRoles(searchQueryChoice) + if (relatedRoles?.length) { + searchQueries = searchQueries.concat(relatedRoles) + } + } + + // Perform the search + results = fuzzySearchByArray( + allProjects, + [{ name: searchField, weight: 1 }], + searchQueries, + ) as Projects + + // Create a description for the search + const description = `${searchField}: "${searchQueryChoice}"` + + // Navigate to the updated list with the search results + navigate(screens.list[params.type], { + type: ListType.Projects, + search: { + results, + description, + } as ProjectSearch, + } as ListRouteParams) + } + return ( <> - navigate(screens.search[params.type], '')} - /> - - Projects List - {/* TODO: reinstate when functionality is ready */} - {/* */} - - {params?.type && listItemsToShow ? ( - <> - {/* Past / Upcoming / My Events choice */} - {params.type === ListType.Events && ( - - )} - - {/* Quick search for upcoming events (Today / This week / This month) */} - {params.type === ListType.Events && - eventsShowUpcomingQuickSearch && - eventsQuickSearchUpcomingChoice && ( - - + {/* TODO: reinstate when functionality is ready */} + {/* */} + + + + + + + + + {/* TODO remove button once not needed anymore for testing SVA-444 */} + - + } + /> + + + + + + + + + + + {params?.type && listItemsToShow ? ( + <> + {/* Past / Upcoming / My Events choice */} + {params.type === ListType.Events && ( + )} - {/* If the user has searched, show some text indicating what they searched for - and give them the option to clear the search */} - {params?.search && ( - - {params?.search?.description && ( - - Results for {params.search.description} - + {/* Quick search for upcoming events (Today / This week / This month) */} + {params.type === ListType.Events && + eventsShowUpcomingQuickSearch && + eventsQuickSearchUpcomingChoice && ( + + + )} - - Clear search - - - )} - - {/* Projects filter & sort options */} - {params.type === ListType.Projects && - Boolean(params?.search) && - Boolean(listItemsToShow.length) && } - - - - ) : ( - <> - - - - - )} + + + + ) : ( + <> + + + + + + + )} + ) diff --git a/src/NativeBase/Containers/MyExperienceContainer.tsx b/src/NativeBase/Containers/MyExperienceContainer.tsx index c9632774..2b751605 100644 --- a/src/NativeBase/Containers/MyExperienceContainer.tsx +++ b/src/NativeBase/Containers/MyExperienceContainer.tsx @@ -10,6 +10,8 @@ import { View, Text, Button, + IconButton, + Icon, } from 'native-base' import ProfileButtons from '../Components/ProfileButtons' import ProgressBar from '../Components/ProgressBar' @@ -22,6 +24,7 @@ import { getProgressBarColors, nextScreen, } from '../../Utils/ProgressBarColours' +import MaterialIcons from 'react-native-vector-icons/MaterialIcons' const MyExperienceContainer = () => { const [skillsValue, setSkillsValue] = React.useState([]) // skills selected by the user @@ -47,23 +50,53 @@ const MyExperienceContainer = () => { return ( <> - + {/* My Experience - + */} {/* */} - - null} - handleChangeText={(text: string) => setSearchTxt(text)} - /> - + {/* */} + + + + null} + handleChangeText={(text: string) => setSearchTxt(text)} + /> + + + + + } + /> + + + + {/* */} {filteredSkills.map((roleGroup: RoleGroup, index: number) => ( @@ -108,7 +141,7 @@ const MyExperienceContainer = () => { ))} - + {/* { Skip )} - + */} ) diff --git a/src/NativeBase/Containers/ProjectSearchContainer.tsx b/src/NativeBase/Containers/ProjectSearchContainer.tsx deleted file mode 100644 index 6bc1b6fa..00000000 --- a/src/NativeBase/Containers/ProjectSearchContainer.tsx +++ /dev/null @@ -1,248 +0,0 @@ -/** - * @file Projects search screen container. - */ - -import React, { useState } from 'react' -import Fuse from 'fuse.js' // fuzzy text search - see docs at https://fusejs.io -import { ScrollView, VStack } from 'native-base' -import { useSelector } from 'react-redux' -import { ListRouteParams, ListSearch, ListType } from './ListContainer' -import ChoicesList, { - ChoicesListChoice, - ChoicesListColour, - ChoicesListFontStyle, -} from '../Components/ChoicesList' -import FreeSearchBar from '../Components/FreeSearchBar' -import SegmentedPicker, { - SegmentedPickerOption, -} from '../Components/SegmentedPicker' -import { navigate, RootStackParamList } from '@/Navigators/utils' -import { - Projects, - ProjectsSearchField, - ProjectSector, - ProjectTechnology, -} from '@/Services/modules/projects' -import { - roleGroups, - RoleGroupName, -} from '@/Services/modules/projects/roleGroups' -import { ProjectsState } from '@/Store/Projects' -import { searchByArray, fuzzySearchByArray } from '@/Utils/Search' - -enum Tab { - Roles = 'Roles', - Tech = 'Tech', - Causes = 'Causes', -} - -export interface ProjectSearch extends ListSearch { - results: Projects // the projects results for this search -} - -/** - * Container for the user to search projects e.g. by free text, category, skills - * - * @returns {React.ReactElement} Component - */ -const ProjectSearchContainer = () => { - const allProjects = useSelector( - (state: { projects: ProjectsState }) => state.projects.projects, - ) - const [selectedTab, setSelectedTab] = useState(Tab.Roles) - const tabs = Object.values(Tab).map( - tab => - ({ - text: tab, - onPress: () => setSelectedTab(tab), - isSelected: tab === selectedTab, - } as SegmentedPickerOption), - ) - - // Define which quick search options to use - - const quickSearchRoleGroupNames: RoleGroupName[] = [ - RoleGroupName.WebDeveloper, - RoleGroupName.TechSupport, - RoleGroupName.UIUX, - RoleGroupName.Researcher, - RoleGroupName.BAPM, - RoleGroupName.ScrumMaster, - ] - const quickSearchRoleChoices = quickSearchRoleGroupNames.map( - roleGroupName => - ({ - text: roleGroupName, - onPress: () => - handleQuickSearchSubmit(ProjectsSearchField.Role, roleGroupName), - } as ChoicesListChoice), - ) - - const quickSearchTechnologies = Object.values(ProjectTechnology).map( - technology => - ({ - text: technology, - onPress: () => - handleQuickSearchSubmit(ProjectsSearchField.Skills, technology), - } as ChoicesListChoice), - ) - - const quickSearchCauses = Object.values(ProjectSector).map( - cause => - ({ - text: cause, - onPress: () => - handleQuickSearchSubmit(ProjectsSearchField.Sector, cause), - } as ChoicesListChoice), - ) - - // Define colour and style to use for quick search options - const quickSearchListColour = ChoicesListColour.primary - const quickSearchListStyle = ChoicesListFontStyle.mediumLight - - // Ensure job title searches find related roles - const getRelatedRoles = ( - possibleRoleSearchQuery: string, - ): string[] | undefined => { - const fuse = new Fuse(roleGroups, { - keys: ['roleNames'], - minMatchCharLength: 2, - threshold: 0.1, - }) - - const fuseResults = fuse.search(possibleRoleSearchQuery) - - if (fuseResults.length) { - const roles = [] - - for (const fuseResult of fuseResults) { - for (const role of fuseResult.item.roleNames) { - roles.push(role) - } - } - - return roles - } - - return undefined - } - - const handleQuickSearchSubmit = ( - searchField: ProjectsSearchField, - searchQueryChoice: string, - ) => { - let searchQueries = [] as string[] - let results = [] as Projects - - searchQueries.push(searchQueryChoice) - - if (searchField === 'role') { - const relatedRoles = getRelatedRoles(searchQueryChoice) - - if (relatedRoles?.length) { - searchQueries = searchQueries.concat(relatedRoles) - } - results = fuzzySearchByArray( - allProjects, - [searchField], - searchQueries, - ) as Projects // we need to use fuzzy search as the roles names are not exact (charities use different ways of naming roles) - } else { - results = searchByArray( - allProjects, - searchField, - searchQueries, - ) as Projects // here we do not want to use fuzzy search as it would include unwanted results - } - - const description = `${ - searchField === 'sector' ? 'cause' : searchField - } "${searchQueryChoice}"` - - navigate( - 'SearchResults' as keyof RootStackParamList, - { - type: ListType.Projects, - search: { - results, - description, - } as ProjectSearch, - } as ListRouteParams, - ) - } - - const handleFreeTextSubmit = (freeTextSearchQuery: string) => { - // Add free text to list of search queries - const searchQueries = [freeTextSearchQuery] - - // If the free text query matches a group of job roles, add these to the list of search queries too - const relatedRoles = getRelatedRoles(freeTextSearchQuery) - if (relatedRoles?.length) { - searchQueries.push(...relatedRoles) - } - - const results = fuzzySearchByArray( - allProjects, - [ - { name: 'client', weight: 1 }, - // We reduce the 'weight' (aka importance) put on the description field as it's more likely to return - // false positive matches because there's lots of general text in that field - { name: 'description', weight: 0.5 }, - { name: 'name', weight: 1 }, - { name: 'role', weight: 1 }, - { name: 'skills', weight: 1 }, - { name: 'sector', weight: 1 }, - ], - searchQueries, - ) - - const description = `"${freeTextSearchQuery}"` - - navigate( - 'SearchResults' as keyof RootStackParamList, - { - type: ListType.Projects, - search: { - results, - description, - } as ProjectSearch, - } as ListRouteParams, - ) - } - - return ( - - - - - - - {selectedTab === Tab.Roles && ( - - )} - - {selectedTab === Tab.Tech && ( - - )} - - {selectedTab === Tab.Causes && ( - - )} - - - ) -} - -export default ProjectSearchContainer diff --git a/src/NativeBase/Containers/SearchResultsContainer.tsx b/src/NativeBase/Containers/SearchResultsContainer.tsx index c0cd090e..f97b9770 100644 --- a/src/NativeBase/Containers/SearchResultsContainer.tsx +++ b/src/NativeBase/Containers/SearchResultsContainer.tsx @@ -2,7 +2,15 @@ * @file Search results screen container. */ -import React, { useState } from 'react' +import { EventSearch } from '@/Containers/EventSearchContainer' +import List, { + ListDisplayMode, + ListOptions, +} from '@/NativeBase/Components/List' +import SearchIconHighlighted from '@/NativeBase/Components/SearchIconHighlighted' +import { goBack, navigate, RootStackParamList } from '@/Navigators/utils' +import { Events } from '@/Services/modules/events' +import { Projects } from '@/Services/modules/projects' import { Box, HStack, @@ -12,17 +20,10 @@ import { Text, View, } from 'native-base' +import React, { useState } from 'react' import { Dimensions } from 'react-native' import MaterialIcons from 'react-native-vector-icons/MaterialIcons' -import { EventSearch } from '@/Containers/EventSearchContainer' -import List, { - ListDisplayMode, - ListOptions, -} from '@/NativeBase/Components/List' -import SearchIconHighlighted from '@/NativeBase/Components/SearchIconHighlighted' -import { goBack, navigate, RootStackParamList } from '@/Navigators/utils' -import { Events } from '@/Services/modules/events' -import { Projects } from '@/Services/modules/projects' +import { ProjectSearch } from '../Components/ProjectTagButtonsFilter' import { ListRouteParams, ListScreens, @@ -30,7 +31,6 @@ import { ListType, searchScreens, } from './ListContainer' -import { ProjectSearch } from './ProjectSearchContainer' export interface SearchResults extends ListSearch { results: Events | Projects diff --git a/src/NativeBase/Containers/VerticalStackContainer.tsx b/src/NativeBase/Containers/VerticalStackContainer.tsx deleted file mode 100644 index bda67477..00000000 --- a/src/NativeBase/Containers/VerticalStackContainer.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/** - * @file A generalised stacked container used for events or projects. - */ -import React from 'react' -import { - Box, - FlatList, - Text, - Heading, - Button, - HStack, - FavouriteIcon, - VStack, -} from 'native-base' -import TechBadge from '../Components/TechBadge' -import TopOfApp from '../Components/TopOfApp' -import { ColorType } from 'native-base/lib/typescript/components/types' - -type TechBadge = { - caption: string - color: ColorType -} - -const data = [ - { - id: '1', - title: 'Fyne Futures Charity Inventory Integration', - charity: 'Fyne Futures Ltd', - role: 'Business analyst', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', - badge: [ - { caption: 'Analysis', color: 'primary' }, - { caption: 'Data', color: 'secondary' }, - ], - hours: '1-4 hours per week', - buddying: true, - }, - { - id: '2', - title: 'Fyne Futures Charity Inventory Integration', - charity: 'Fyne Futures Ltd', - role: 'Frontend Developer', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', - badge: [{ caption: 'Frontend', color: 'primary' }], - hours: '1-4 hours per week', - buddying: true, - }, - { - id: '3', - title: 'Fyne Futures Charity Inventory Integration', - charity: 'Fyne Futures Ltd', - role: 'Backend Developer', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', - badge: [{ caption: 'Backend', color: 'primary' }], - hours: '1-4 hours per week', - buddying: true, - }, -] - -const VerticalStackContainer = () => { - return ( - <> - - - ( - - - {item.title} - {item.charity} - {item.role} - {item.description} - - {item.hours} - - {item.buddying - ? 'Suitable for buddying' - : 'Not suitable for buddiing'} - - - - - - - - )} - keyExtractor={item => item.id} - /> - - ) -} - -export default VerticalStackContainer diff --git a/src/NativeBase/Containers/index.ts b/src/NativeBase/Containers/index.ts index 8cac1f00..548d60f0 100644 --- a/src/NativeBase/Containers/index.ts +++ b/src/NativeBase/Containers/index.ts @@ -2,12 +2,10 @@ * @file Exports the containers for easy access elsewhere. */ +export { default as ProjectsTagButtonsFilter } from '../Components/ProjectTagButtonsFilter' export { default as ListContainer } from './ListContainer' +export { default as LoginContainer } from './LoginContainer' export { default as ProfileContainer } from './ProfileContainer' export { default as ProjectDetailContainer } from './ProjectDetailContainer' -export { default as ProjectSearchContainer } from './ProjectSearchContainer' -export { default as SearchResultsContainer } from './SearchResultsContainer' export { default as SettingsContainer } from './SettingsContainer' -export { default as VerticalStackContainer } from './VerticalStackContainer' export { default as WebViewContainer } from './WebViewContainer' -export { default as LoginContainer } from './LoginContainer' diff --git a/src/Navigators/Application.tsx b/src/Navigators/Application.tsx index f9a3e0f7..df7fbbfd 100644 --- a/src/Navigators/Application.tsx +++ b/src/Navigators/Application.tsx @@ -2,37 +2,35 @@ * @file Defines the list of screens (apart from the main screens that have tabs at the bottom of the app e.g. Projects -- these are defined in Main.tsx). */ -import { useColorMode, View } from 'native-base' -import React, { useEffect, useRef, useState } from 'react' -import { AppState, StatusBar, useColorScheme } from 'react-native' -import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { NavigationContainer } from '@react-navigation/native' -import { - createStackNavigator, - StackHeaderProps, - StackNavigationOptions, -} from '@react-navigation/stack' -import { useSelector } from 'react-redux' import { EventDetailContainer, - StartupContainer, EventSearchContainer, + StartupContainer, WelcomeContainer, } from '@/Containers' -import MyExperienceContainer from '@/NativeBase/Containers/MyExperienceContainer' -import ProjectRegisterInterestContainer from '@/NativeBase/Containers/ProjectRegisterInterestContainer' -import MainNavigator from './Main' -import { navigationRef } from './utils' import NavigationHeader from '@/NativeBase/Components/NavigationHeader' import { LoginContainer, ProjectDetailContainer, - ProjectSearchContainer, - SearchResultsContainer, WebViewContainer, } from '@/NativeBase/Containers' +import MyExperienceContainer from '@/NativeBase/Containers/MyExperienceContainer' +import ProjectRegisterInterestContainer from '@/NativeBase/Containers/ProjectRegisterInterestContainer' import StaTheme from '@/NativeBase/Theme/StaTheme' import { ThemeState } from '@/Store/Theme' +import { NavigationContainer } from '@react-navigation/native' +import { + createStackNavigator, + StackHeaderProps, + StackNavigationOptions, +} from '@react-navigation/stack' +import { useColorMode, View } from 'native-base' +import React, { useEffect, useRef, useState } from 'react' +import { AppState, StatusBar, useColorScheme } from 'react-native' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { useSelector } from 'react-redux' +import MainNavigator from './Main' +import { navigationRef } from './utils' const Stack = createStackNavigator() @@ -166,23 +164,14 @@ const ApplicationNavigator = () => { }} /> - - - + /> */} { return ( - {/* */}