diff --git a/client/src/AppRoutes.tsx b/client/src/AppRoutes.tsx index 2664860ae..223398efe 100644 --- a/client/src/AppRoutes.tsx +++ b/client/src/AppRoutes.tsx @@ -48,6 +48,7 @@ import NewEquipmentPage from "./pages/makerspace_page/equipment_pages/NewEquipme import EquipmentRedirector from "./pages/makerspace_page/equipment_pages/EquipmentRedirector"; import HelpPage from "./pages/maker/signup/HelpPage"; import UserPage from "./pages/lab_management/users/UserPage"; +import GlobalSearchPage from "./pages/both/globalsearch/GlobalSearchPage"; // This is where we map the browser's URL to a // React component with the help of React Router. @@ -126,6 +127,7 @@ export default function AppRoutes() { } /> } /> } /> + } /> {/* Routes that need to be protected by auth */} }> diff --git a/client/src/pages/both/globalsearch/GlobalSearchPage.tsx b/client/src/pages/both/globalsearch/GlobalSearchPage.tsx new file mode 100644 index 000000000..cd1d358f3 --- /dev/null +++ b/client/src/pages/both/globalsearch/GlobalSearchPage.tsx @@ -0,0 +1,93 @@ +import { Divider, Grid, Stack, Typography } from "@mui/material"; +import { useNavigate, useParams } from "react-router-dom"; +import Equipment from "../../../types/Equipment"; +import { GET_EQUIPMENTS } from "../../../queries/equipmentQueries"; +import { useQuery } from "@apollo/client"; +import EquipmentCard from "../../../common/EquipmentCard"; +import { useIsMobile } from "../../../common/IsMobileProvider"; +import { useCurrentUser } from "../../../common/CurrentUserProvider"; +import RequestWrapper from "../../../common/RequestWrapper"; +import { GET_PUBLISHED_TRAINING_MODULES } from "../../../queries/trainingQueries"; +import { ModuleStatus, moduleStatusMapper, TrainingModule } from "../../../common/TrainingModuleUtils"; +import ModuleStatusRow from "../../../common/ModuleStatusRow"; +import { GET_MAKERSPACES_WITH_HOURS, MakerspaceWithHours } from "../../../queries/makerspaceQueries"; +import MakerspaceCard from "../homepage/MakerspaceCard"; + +export default function GlobalSearchPage (){ + const {query} = useParams(); + const user = useCurrentUser(); + const isMobile = useIsMobile(); + const navigate = useNavigate(); + + const getEquipment = useQuery(GET_EQUIPMENTS); + const filteredEquipment = getEquipment.data?.equipments.filter((equipment: Equipment) => equipment.name.toLowerCase().includes(query!.toLowerCase())); + const foundEquipments = filteredEquipment?.map((equipment: Equipment) => ( + + + + )); + + + const getTrainings = useQuery(GET_PUBLISHED_TRAINING_MODULES); + const filteredTrainings = getTrainings.data?.publishedModules.filter((training: TrainingModule) => training.name.toLowerCase().includes(query!.toLowerCase())); + const moduleStatuses = filteredTrainings?.map( + moduleStatusMapper(user.passedModules, user.trainingHolds) + ); + const foundTrainings = moduleStatuses?.map((moduleStatus: ModuleStatus) => ( + + + + )); + + const getMakerspaces = useQuery(GET_MAKERSPACES_WITH_HOURS) + const filteredMakerspaces = getMakerspaces.data?.makerspaces.filter((makerspace: MakerspaceWithHours) => makerspace.name.toLowerCase().includes(query!.toLowerCase())) + const foundMakerspaces = filteredMakerspaces?.map((makerspace: MakerspaceWithHours) => + + + + ) + + return( + + }> + {`${query} - Search Results`} + Equipment + {foundEquipments?.length > 0 ? + + {foundEquipments} + + : No Equipment Found + } + + Trainings + {foundTrainings?.length > 0 ? + + {foundTrainings} + + : No Trainings Found + } + + Makerspaces + {foundMakerspaces?.length > 0 ? + + {foundMakerspaces} + + : No Makerspaces Found + } + + + + ); +} \ No newline at end of file diff --git a/client/src/queries/equipmentQueries.ts b/client/src/queries/equipmentQueries.ts index a42271c51..757444b57 100644 --- a/client/src/queries/equipmentQueries.ts +++ b/client/src/queries/equipmentQueries.ts @@ -35,6 +35,21 @@ export const GET_ALL_EQUIPMENTS = gql` } `; +export const GET_ALL_PUBLISHED_EQUIPMENTS = gql` + query GetAllPublishedEquipment { + equipments { + id + name + archived + room { + makerspace { + id + } + } + } + } +` + export const GET_EQUIPMENT_BY_ID = gql` query GetEquipmentByID($id: ID!) { equipment(id: $id) { diff --git a/client/src/queries/trainingQueries.ts b/client/src/queries/trainingQueries.ts index fa5f5be51..370e53812 100644 --- a/client/src/queries/trainingQueries.ts +++ b/client/src/queries/trainingQueries.ts @@ -62,6 +62,17 @@ export const GET_ARCHIVED_TRAINING_MODULES = gql` } `; +export const GET_PUBLISHED_TRAINING_MODULES = gql` + query GetPublishedTrainingModules { + publishedModules { + id + name + archived + makerspaceID + } + } +` + export const CREATE_TRAINING_MODULE = gql` mutation CreateTrainingModule($name: String!, $quiz: JSON!, $makerspaceID: ID) { createModule(name: $name, quiz: $quiz, makerspaceID: $makerspaceID) { diff --git a/client/src/top_nav/GlobalSearchBar.tsx b/client/src/top_nav/GlobalSearchBar.tsx new file mode 100644 index 000000000..5b622851a --- /dev/null +++ b/client/src/top_nav/GlobalSearchBar.tsx @@ -0,0 +1,78 @@ +import { Autocomplete, TextField } from "@mui/material"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { GET_ALL_PUBLISHED_EQUIPMENTS } from "../queries/equipmentQueries"; +import { useQuery } from "@apollo/client"; +import { GET_PUBLISHED_TRAINING_MODULES } from "../queries/trainingQueries"; +import GET_ROOMS from "../queries/roomQueries"; +import { FullMakerspace, GET_FULL_MAKERSPACES, GET_MAKERSPACES } from "../queries/makerspaceQueries"; +import Equipment from "../types/Equipment"; +import { TrainingModule } from "../common/TrainingModuleUtils"; +import Room from "../types/Room"; + +export default function GlobalSearchBar() { + const navigate = useNavigate(); + const [searchQuery, setSearchQuery] = useState(""); + + const getEquipment = useQuery(GET_ALL_PUBLISHED_EQUIPMENTS) + const getTrainings = useQuery(GET_PUBLISHED_TRAINING_MODULES) + const getRooms = useQuery(GET_ROOMS) + const getMakerspaces = useQuery(GET_FULL_MAKERSPACES) + + const options: any[] = []; + getEquipment.data?.equipments.forEach((equipment:Equipment) => { + options.push({label:equipment.name, category:"Equipments", item:equipment}) + }); + getTrainings.data?.publishedModules.forEach((module:TrainingModule) => { + options.push({label:module.name, category:"Trainings", item:module}) + }); + getRooms.data?.rooms.forEach((room:Room) => { + options.push({label:room.name, category:"Rooms", item:room}) + }); + getMakerspaces.data?.makerspaces.forEach((makerspace:FullMakerspace) => { + options.push({label:makerspace.name, category:"Makerspaces", item:makerspace}) + }); + + const handleRedirect = (reason:string, value:any) => { + switch(reason){ + case 'input': + setSearchQuery(value) + break + case 'createOption': + const encodedQuery = searchQuery.replace('/', '%2F') + navigate(`/search/` + encodedQuery) + break + case 'selectOption': + const category = value.category + const encodedLabel = value.label.replace('/', '%2F') + if(category === "Equipments"){ + navigate(`/makerspace/` + value.item.room.makerspace.id + `?a=${encodedLabel}`) + } + else if(category === "Trainings"){ + navigate(`/maker/training/${value.item.id}`) + } + else if(category === "Makerspaces"){ + navigate(`/makerspace/${value.item.id}`) + } + else{ + navigate(`/search/` + encodedLabel) + break + } + } + }; + + return ( + option.category} + sx={{ width: 300 }} + freeSolo={true} + autoHighlight={false} + blurOnSelect={true} + onChange={(e, v, r) => {r === 'selectOption' || r === 'createOption' ? handleRedirect(r, v === null ? "" : v) : {}}} + onInputChange={(e, v: string, r) => r === 'input' ? handleRedirect(r, v) : {}} + renderInput={(params) => {event.target.select()}}/>} + /> + ); +} \ No newline at end of file diff --git a/client/src/top_nav/TopNav.tsx b/client/src/top_nav/TopNav.tsx index cad7da369..188abad39 100644 --- a/client/src/top_nav/TopNav.tsx +++ b/client/src/top_nav/TopNav.tsx @@ -18,6 +18,7 @@ import StorefrontIcon from '@mui/icons-material/Storefront'; import ArticleIcon from '@mui/icons-material/Article'; import TuneIcon from '@mui/icons-material/Tune'; import LogoutIcon from '@mui/icons-material/Logout'; +import GlobalSearchBar from "./GlobalSearchBar"; const StyledLogo = styled.img` padding: 12px; @@ -182,8 +183,10 @@ export default function TopNav() { {navlinks} + {userProfileButton} {userMenu} + diff --git a/server/src/resolvers/trainingModuleResolver.ts b/server/src/resolvers/trainingModuleResolver.ts index c8ac2c71b..eb05c7a74 100644 --- a/server/src/resolvers/trainingModuleResolver.ts +++ b/server/src/resolvers/trainingModuleResolver.ts @@ -252,6 +252,14 @@ const TrainingModuleResolvers = { return module; }), + publishedModules: async ( + _parent: any, + _args: any, + ) => { + return await ModuleRepo.getModulesWhereArchived(false); + }, + + /** * Fetch an array of AccessProgress items representing progress on gaining access to all equipment relating to the noted TrainingModule * @argument sourceTrainingModuleID ID of TrainingModule to source from diff --git a/server/src/schemas/trainingModuleSchema.ts b/server/src/schemas/trainingModuleSchema.ts index 421f4224e..e7cfe650b 100644 --- a/server/src/schemas/trainingModuleSchema.ts +++ b/server/src/schemas/trainingModuleSchema.ts @@ -56,6 +56,7 @@ export const TrainingModuleTypeDefs = gql` module(id: ID!): TrainingModule moduleWithAnswers(id: ID!): TrainingModule archivedModules: [TrainingModule] + publishedModules: [TrainingModule] archivedModule(id: ID!): TrainingModule relatedAccessProgress(sourceTrainingModuleID: ID!): [AccessProgress] }