Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement and Refactor Search Functionality for Tutorials #150

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Routes from "./routes";
import "./App.less";
import { useFirebase, useFirestore } from "react-redux-firebase";
import { useDispatch, useSelector } from "react-redux";
import { getProfileData } from "./store/actions";
import { getProfileData, fetchAndIndexTutorials } from "./store/actions";

const App = () => {
const firebase = useFirebase();
Expand All @@ -19,6 +19,7 @@ const App = () => {

useEffect(() => {
getProfileData(organizations)(firebase, firestore, dispatch);
fetchAndIndexTutorials()(firebase, firestore, dispatch);
}, [organizations, firebase, dispatch]);
return <Routes />;
};
Expand Down
28 changes: 25 additions & 3 deletions src/components/NavBar/new/MainNavbar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,21 @@ function MainNavbar() {
const windowSize = useWindowSize();
const [openDrawer, setOpenDrawer] = useState(false);
const [openMenu, setOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const toggleSlider = () => {
setOpen(!openMenu);
};
const notification = () => {};
const handleSearchChange = e => {
setSearchQuery(e.target.value);
};
const handleSearch = e => {
e.preventDefault();
if (searchQuery.length > 0) {
history.push(`/search?query=${searchQuery}`);
}
};

return (
<Headroom>
<nav
Expand Down Expand Up @@ -105,17 +116,28 @@ function MainNavbar() {
</Grid>
</Grid>
<Grid item xs={12} md={5}>
<Paper component={"form"} className={classes.root} elevation={0}>
<Paper
component={"form"}
className={classes.root}
elevation={0}
onSubmit={handleSearch}
>
<IconButton
type="submit"
type="button"
aria-label="search"
disableRipple
className={classes.icon}
data-testid="navbarSearch"
onClick={handleSearch}
>
<SearchIcon />
</IconButton>
<InputBase className={classes.input} placeholder="Search..." />
<InputBase
className={classes.input}
value={searchQuery}
placeholder="Search..."
onChange={handleSearchChange}
/>
</Paper>
</Grid>
<Grid
Expand Down
19 changes: 18 additions & 1 deletion src/components/NavBar/new/MiniNavbar/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import MenuIcon from "@mui/icons-material/Menu";
import CloseIcon from "@mui/icons-material/Close";
import SideBar from "../../../SideBar";
import useWindowSize from "../../../../helpers/customHooks/useWindowSize";
import { useDispatch } from "react-redux";
import { searchFromTutorialsIndex } from "../../../../store/actions";

const useStyles = makeStyles(theme => ({
input: {
Expand Down Expand Up @@ -66,6 +68,7 @@ function MiniNavbar() {
const classes = useStyles();

const history = useHistory();
const dispatch = useDispatch();
const notification = () => {};
const [openDrawer, setOpenDrawer] = React.useState(false);
const [openMenu, setOpen] = useState(false);
Expand Down Expand Up @@ -95,6 +98,17 @@ function MiniNavbar() {
});
};

const [searchQuery, setSearchQuery] = useState("");
const handleSearchChange = e => {
setSearchQuery(e.target.value);
};
const handleSearch = () => {
if (searchQuery.length > 0) {
dispatch(searchFromTutorialsIndex(searchQuery));
history.push(`/search?query=${searchQuery}`);
}
};

useEffect(() => {
window.addEventListener("resize", setDimension);

Expand Down Expand Up @@ -145,11 +159,12 @@ function MiniNavbar() {
<Grid style={{ display: "inline-block" }} item xs={12} md={4}>
<Paper component={"form"} className={classes.root} elevation={0}>
<IconButton
type="submit"
type="button"
aria-label="search"
disableRipple
className={classes.icon}
data-testid="navbarSearch"
onClick={handleSearch}
>
<SearchIcon />
</IconButton>
Expand All @@ -164,6 +179,8 @@ function MiniNavbar() {
}}
className={classes.input}
placeholder="Search..."
value={searchQuery}
onChange={handleSearchChange}
/>
</Paper>
</Grid>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const TutorialCard = ({
{title}
</Typography>
<Typography variant="body2" color="textSecondary" component="p">
{title}
{summary}
</Typography>
{loading ? <Skeleton variant="text" /> : null}
{loading ? <Skeleton variant="text" /> : null}
Expand Down
131 changes: 121 additions & 10 deletions src/components/Tutorials/MyTutorials/Search/SearchResultsComponent.jsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,135 @@
import Divider from "@mui/material/Divider";
import Grid from "@mui/material/Grid";
import Typography from "@mui/material/Typography";
import React from "react";
import React, { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { useFirebase, useFirestore } from "react-redux-firebase";
import TutorialCard from "../BaseTutorialsComponent/TutorialCard";
import { searchFromTutorialsIndex } from "../../../../store/actions";
import { getTutorialFeedData } from "../../../../store/actions/tutorialPageActions";
import CardWithPicture from "../../../Card/CardWithPicture";
import CardWithoutPicture from "../../../Card/CardWithoutPicture";

const SearchResultsComponent = ({ results: propResults }) => {
const [results, setResults] = useState([]);
const location = useLocation();
const firebase = useFirebase();
const firestore = useFirestore();
const dispatch = useDispatch();

const tutorials = useSelector(
({
tutorialPage: {
feed: { homepageFeedArray }
}
}) => homepageFeedArray
);

useEffect(() => {
setResults(tutorials);
}, [tutorials]);

useEffect(() => {
const query = new URLSearchParams(location.search).get("query");
let tutorialIdArray = propResults || [];

if (query) {
tutorialIdArray = searchFromTutorialsIndex(query);
tutorialIdArray = tutorialIdArray.map(tutorial => tutorial.ref);
}

const getResults = async () => {
if (tutorialIdArray.length > 0) {
await getTutorialFeedData(tutorialIdArray)(
firebase,
firestore,
dispatch
);
} else {
setResults([]);
}
};

getResults();
}, [location.search, propResults, firebase, firestore, dispatch]);

const SearchResultsComponent = ({ results }) => {
return (
<div>
<Grid container item justify="center">
<Divider variant="middle" />
<Grid item xs={12}>
<Typography align="center"> Search Results</Typography>
<Typography align="center" variant="h4">
Search Results
</Typography>
</Grid>
{propResults && (
<>
<Divider variant="middle" />
<Grid container spacing={3}>
{results.map((tutorial, index) => (
<Grid
item
key={index}
xs={12}
sm={6}
md={3}
lg={2}
xl={2}
className="pr-24"
>
<TutorialCard tutorialData={tutorial} loading={false} />
</Grid>
))}
</Grid>
</>
)}

<Grid
container
spacing={3}
justifyContent="center"
alignItems="center"
sx={{
padding: "1rem",
margin: "0 auto",
width: "100%",
maxWidth: "1200px"
}}
>
{!propResults &&
results.map((tutorial, index) => (
<Grid
item
key={index}
xs={12}
sm={6}
md={6}
lg={6}
xl={6}
sx={{ mb: 2 }}
>
{!tutorial?.featured_image ? (
<CardWithoutPicture tutorial={tutorial} />
) : (
<CardWithPicture tutorial={tutorial} />
)}
</Grid>
))}
</Grid>

<Grid container justifyContent="center">
{results.length === 0 && (
<Typography
variant="h6"
component="p"
color="textSecondary"
sx={{ marginTop: 2 }}
>
No CodeLabz found with the given query.
</Typography>
)}
</Grid>
<Divider variant="middle" />
{results.map((tutorial, index) => (
<Grid xs={12} sm={6} md={3} lg={2} xl={2} className="pr-24">
<TutorialCard key={index} tutorialData={tutorial} loading={false} />
</Grid>
))}
{results.length === 0 && "No CodeLabz with the given query"}

<Divider variant="middle" />
</Grid>
Expand Down
5 changes: 1 addition & 4 deletions src/components/Tutorials/MyTutorials/Search/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,7 @@ const Header = () => {
if (result.length > 0) {
let tempArray = [];
result.forEach(item => {
tempArray = [
...tempArray,
..._.filter(indexData, ref => ref.tutorial_id === item.ref)
];
tempArray = [...tempArray, item.ref];
});
setViewResults(true);
return setResults(tempArray);
Expand Down
32 changes: 28 additions & 4 deletions src/helpers/elasticlunr.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import elasticlunr from "elasticlunr";

export default class Elasticlunr {
constructor(key, ...fields) {
this.elasticlunr = elasticlunr();
Expand All @@ -8,11 +9,34 @@ export default class Elasticlunr {
});
}

addDocToIndex = doc => {
this.elasticlunr.addDoc(doc);
addDocToIndex(doc) {
try {
this.elasticlunr.addDoc(doc);
} catch (error) {
console.error("Error adding document to index:", error);
}
}

searchFromIndex(query, options = { expand: true }) {
try {
if (typeof query !== "string") {
throw new Error("Query must be a string.");
}

return this.elasticlunr.search(query, options);
} catch (error) {
console.error("Error searching the index:", error);
return [];
}
}

// To get a specific document by its reference key
getDocById = id => {
return this.elasticlunr.documentStore.getDoc(id);
};

searchFromIndex = query => {
return this.elasticlunr.search(query, { expand: true });
// To get all documents in the index
getAllDocs = () => {
return Object.values(this.elasticlunr.documentStore.docs);
};
}
2 changes: 2 additions & 0 deletions src/routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import MainNavbar from "./components/NavBar/new/MainNavbar";
import UserDashboard from "./components/UserDashboard";
import TutorialPage from "./components/TutorialPage";
import Notification from "./components/Notification";
import SearchResultsComponent from "./components/Tutorials/MyTutorials/Search/SearchResultsComponent";

const AuthIsLoaded = ({ children }) => {
const profile = useSelector(({ firebase: { profile } }) => profile);
Expand Down Expand Up @@ -157,6 +158,7 @@ const Routes = () => {
path={"/tutorial/:id"}
component={UserIsAllowedUserDashboard(TutorialPage)}
/>
<Route exact path={"/search"} component={SearchResultsComponent} />
<Route
exact
path={"/editor"}
Expand Down
3 changes: 2 additions & 1 deletion src/store/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,6 @@ export {
setTutorialTheme,
updateStepTime,
updateStepTitle,
uploadTutorialImages
uploadTutorialImages,
fetchAndIndexTutorials,
} from "./tutorialsActions";
20 changes: 19 additions & 1 deletion src/store/actions/tutorialsActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,26 @@ const tutorials_index = new Elasticlunr(
"summary"
);

export const fetchAndIndexTutorials = () => async (firebase, firestore) => {
try {
const snapshot = await firestore.collection("tutorials").get();
snapshot.forEach(doc => {
const data = doc.data();
const tutorial = { id: doc.id, ...data };
// console.log("Adding tutorial to index:", tutorial);
tutorials_index.addDocToIndex(tutorial);
});

// console.log("All docs in index:", tutorials_index.getAllDocs());
} catch (error) {
console.error("Error fetching or indexing tutorials:", error);
}
};

export const searchFromTutorialsIndex = query => {
return tutorials_index.searchFromIndex(query);
const results = tutorials_index.searchFromIndex(query);
// console.log("searchFromIndex", query, results);
return results;
};

// Gets all the tutorials with this user having edit access
Expand Down
Loading