I'm using Algolia's react instant search and I want to know what code I can use that'll send me to a specific page when I click on a "hit" from the hits widget. I'm using Next.js.
Code:
import React from 'react';
import { useRef, useState, useEffect } from 'react';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch } from 'react-instantsearch-dom';
import { Index } from 'react-instantsearch-dom';
import { Configure } from 'react-instantsearch-dom';
import { Pagination } from 'react-instantsearch-dom';
const searchClient = algoliasearch(
'XXXXXXXXXX',
'XXXXXXXXXXXXXXXXXXXXXXXXXXX'
);
const Hit = ({ hit }) => <p>{hit.title}</p>;
import { connectSearchBox } from 'react-instantsearch-dom';
const SearchBox = ({ currentRefinement, isSearchStalled, refine }) => (
<form noValidate action="" role="search">
<div className="container flex justify-center items-center px-4 sm:px-6 lg:px-8 relative">
<input
type="search"
placeholder='Search Documentation'
value={currentRefinement}
onChange={event => refine(event.currentTarget.value)}
className="h-7 w-96 pr-8 pl-5 rounded z-0 hover:text-gray-500 outline-none border-b-2"
/>
<i className="fa fa-search text-gray-400 z-20 hover:text-gray-500"></i>
</div>
<button onClick={() => refine('')}>Reset query</button>
{isSearchStalled ? 'My search is stalled' : ''}
</form>
);
const CustomSearchBox = connectSearchBox(SearchBox);
import { connectHits } from 'react-instantsearch-dom';
const Hits = ({ hits }) => (
<table className="table-auto">
{hits.map(hit => (
<tbody>
<tr>
<td className="text-black font-bold" key={hit.objectID}>{hit.title}</td>
</tr>
</tbody>
))}
</table>
);
const CustomHits = connectHits(Hits);
import { QueryRuleCustomData } from 'react-instantsearch-dom';
function SearchApp({location, history}) {
const [showHits, setShowHits] = useState(false);
return (
<div>
<>
<InstantSearch
indexName="prod_Directory"
searchClient={searchClient}
>
<Index indexName="prod_Directory">
{/* Widgets */}
<div>
<CustomSearchBox onFocus={()=>setShowHits(true)} onBlur={()=>setShowHits(false)}/>
<CustomHits className="table-auto"/>
{/*
{showHits ? <CustomHits className="table-auto"/> : null}
*/}
</div>
</Index>
<Configure hitsPerPage={2} />
<QueryRuleCustomData
transformItems={items => {
const match = items.find(data => Boolean(data.redirect));
if (match && match.redirect) {
window.location.href = match.redirect;
}
return [];
}}
>
{() => null}
</QueryRuleCustomData>
</InstantSearch>
</>
</div>
)
}
export default SearchApp
I couldn't find anything about this in the Algolia docs. Again, I want to be able to click on one of my hits, and have it redirect or route me to a specific page.
It looks like you're using a custom Hits widget here rather than the out-of-the-box instantsearch.js widget (which is fine).
You're going to want to build you link here in the hit template:
const Hits = ({
hits
}) => ( <
table className = "table-auto" > {
hits.map(hit => ( <
tbody >
<
tr >
<
td className = "text-black font-bold"
key = {
hit.objectID
} > {
hit.title
} < /td> <
/tr>
<
/tbody>
))
} <
/table>
);
For instance if you store the URLs in the object records, you could do something like:
{
hit.title
}
More likely, you'll want to build onClick event using Link. Something like:
<Link
onClick={() => {
setIsOpen(false);
}}
to={`/product/${hit.objectID}`}
>
hit.title
</Link>
In either case, just make sure everything you need to build the link (URL, routing IDs, etc.) is embedded in the Algolia records, then just build your links within your hit template as you typically would for your application.
I found the answer:
import router, {useRouter} from "next/router";
import { connectHits } from 'react-instantsearch-dom';
const Hits = ({ hits }) => (
<table className="table-auto">
{hits.map(hit => (
<tbody>
<tr>
<td className="text-black font-bold" key={hit.objectID} onClick={() => router.push(hit.url)}>{hit.title}</td>
</tr>
</tbody>
))}
</table>
);
const CustomHits = connectHits(Hits);
In your search records (I used the Algolia index to make mine), you just need to code in a url in your JSON record and then use react router on the hit.url attribute!
Related
let me explain my situation.
I am building a MERN project to my portfolio and I am trying to make a button toggle between the name of an item and a inputfield. So when the user click the pen (edit), it will add a class with the displain:none; in the div with the text coming from the MongoDB data base to hide it and will remove it from the div with the input. I could manage to do it. BUT since the amount of items can inscrease, clicking in one of them cause the toggle in all of them.
It was ok until I send some useState as props to the component.
This is my code from the App.jsx
import React, {useState, useEffect} from "react";
import Axios from "axios";
import "./App.css";
import ListItem from "./components/ListItem";
function App() {
//here are the use states
const [foodName, setFoodName] = useState("");
const [days, setDays] = useState(0);
const [newFoodName, setNewFoodName] = useState("");
const [foodList, setFoodList] = useState([]);
//here is just the compunication with the DB of a form that I have above those components
useEffect(() => {
Axios.get("http://localhost:3001/read").then((response) => {
setFoodList(response.data);
});
}, []);
const addToList = () => {
Axios.post("http://localhost:3001/insert", {
foodName: foodName,
days: days,
});
};
const updateFood = (id) => {
Axios.put("http://localhost:3001/update", {
id: id,
newFoodName: newFoodName,
});
};
return (
<div className="App">
//Here it starts the app with the form and everything
<h1>CRUD app with MERN</h1>
<div className="container">
<h3 className="container__title">Favorite Food Database</h3>
<label>Food name:</label>
<input
type="text"
onChange={(event) => {
setFoodName(event.target.value);
}}
/>
<label>Days since you ate it:</label>
<input
type="number"
onChange={(event) => {
setDays(event.target.value);
}}
/>
<button onClick={addToList}>Add to list</button>
</div>
//Here the form finishes and now it starts the components I showed in the images.
<div className="listContainer">
<hr />
<h3 className="listContainer__title">Food List</h3>
{foodList.map((val, key) => {
return (
//This is the component and its props
<ListItem
val={val}
key={key}
functionUpdateFood={updateFood(val._id)}
newFoodName={newFoodName}
setNewFoodName={setNewFoodName}
/>
);
})}
</div>
</div>
);
}
export default App;
Now the component code:
import React from "react";
//Material UI Icon imports
import CancelIcon from "#mui/icons-material/Cancel";
import EditIcon from "#mui/icons-material/Edit";
//import CheckIcon from "#mui/icons-material/Check";
import CheckCircleIcon from "#mui/icons-material/CheckCircle";
//App starts here, I destructured the props
function ListItem({val, key, functionUpdateFood, newFoodName, setNewFoodName}) {
//const [foodList, setFoodList] = useState([]);
//Here I have the handleToggle function that will be used ahead.
const handleToggle = () => {
setNewFoodName(!newFoodName);
};
return (
<div
className="foodList__item"
key={key}>
<div className="foodList__item-group">
<h3
//As you can see, I toggle the classes with this conditional statement
//I use the same classes for all items I want to toggle with one click
//Here it will toggle the Food Name
className={
newFoodName
? "foodList__item-newName-delete"
: "foodList__name"
}>
{val.foodName}
</h3>
<div
className={
newFoodName
? "foodList__item-newName-group"
: "foodList__item-newName-delete"
}>
//Here is the input that will replace the FoodName
<input
type="text"
placeholder="The new food name..."
className="foodList__item-newName"
onChange={(event) => {
setNewFoodName(event.target.value);
}}
/>
//Here it will confirm the update and toggle back
//Didn't implement this yet
<div className="foodList__icons-confirm-group">
<CheckCircleIcon
className="foodList__icons-confirm"
onClick={functionUpdateFood}
/>
<small>Update?</small>
</div>
</div>
</div>
//here it will also desappear on the same toggle
<p
className={
newFoodName
? "foodList__item-newName-delete"
: "foodList__day"
}>
{val.daysSinceIAte} day(s) ago
</p>
<div
className={
newFoodName
? "foodList__item-newName-delete"
: "foodList__icons"
}>
//Here it will update, and it's the button that toggles
<EditIcon
className="foodList__icons-edit"
onClick={handleToggle}
/>
<CancelIcon className="foodList__icons-delete" />
</div>
</div>
);
}
export default ListItem;
I saw a solution that used different id's for each component. But this is dynamic, so if I have 1000 items on the data base, it would display all of them, so I can't add all this id's.
I am sorry for the very long explanation. It seems simple, but since I am starting, I spent the day on it + searched and tested several ways.
:|
working on a cart app in a udemy course - the problem is when the quantity gets bought it supposed to make the button disabled but its not working, only showing the add to cart button without disabling it when quantity are zero
data.countInStock seems not to be updating
import { Button } from 'react-bootstrap';
import { Card } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import React, { useContext } from 'react';
import Rating from './Rating';
import axios from 'axios';
import { Store } from '../Store';
function Product(props){
const {product} = props;
const {state , dispatch:ctxDispatch} = useContext(Store);
const {cart: {cartItems}} = state
const addToCartHandler = async (item )=>{
const existItem = cartItems.find((x)=> x._id === product._id);
const quantity = existItem ? existItem.quantity+1:1 ;
const {data} = await axios.get(`/api/products/${item._id}`);
if(data.countInStock < quantity){
window.alert('sorry product is out of stock')
return;
}
ctxDispatch({
type:'CART_ADD_ITEM'
, payload:{...item , quantity},
});
};
return(
<Card>
<Link to={`/product/${product.slug}`}>
<img src={product.image} className="card-img-top" alt={product.name} />
</Link>
<Card.Body>
<Link to={`/product/${product.slug}`}>
<Card.Title>{product.name}</Card.Title>
</Link>
<Rating rating={product.rating} numReviews={product.numReviews} />
<Card.Text>${product.price}</Card.Text>
{ product.countInStock === 0 ? (
<Button color="light" disabled={true} > Out of stock</Button>
):(
<Button onClick={() => addToCartHandler(product)}>Add to cart</Button>
)}
</Card.Body>
</Card>
)}
it's not showing the button out of stock when quantity gets used, What's wrong with the code?
full code: https://github.com/basir/mern-amazona/commit/12e565bf6e1859b963729eaba46a5352962fe9e1
full code with backend : https://github.com/basir/mern-amazona/tree/12e565bf6e1859b963729eaba46a5352962fe9e1
Maybe this could start you out. There's no need to make 2 buttons. You can just manipulate the state of the button using your logic
const isOutOfStock = product.countInStock === 0
const buttonText = isOutOfStock ? "Out of stock" : "Add to cart"
<Button color="light" disabled={isOutOfStock} onClick={() => addToCartHandler(product)}>{buttonText}</Button>
I use next/image to load my images in my app. It works fine except for a carousel with multiple images.
When I do like this I have that error :
Error: Image is missing required "src" property. Make sure you pass "src" in props to the next/image component. Received: {}
The problem is not because I have an entity without any file
image.js
import { getStrapiMedia } from "../utils/medias"
import NextImage from "next/image"
const Image = (props) => {
if (!props.media) {
return <NextImage {...props} />
}
const { url, alternativeText } = props.media
const loader = ({ src }) => {
return getStrapiMedia(src)
}
return (
<NextImage
loader={loader}
layout="responsive"
objectFit="contain"
width={props.media.width}
height={props.media.height}
src={url}
alt={alternativeText || ""}
/>
)
}
export default Image
Carousel.js
import React, { useCallback } from "react"
import useEmblaCarousel from "embla-carousel-react"
import NextImage from "./Image"
export const EmblaCarousel = (product) => {
const [emblaRef, emblaApi] = useEmblaCarousel()
useEmblaCarousel.globalOptions = { loop: true }
const scrollPrev = useCallback(() => {
if (emblaApi) emblaApi.scrollPrev()
}, [emblaApi])
const scrollNext = useCallback(() => {
if (emblaApi) emblaApi.scrollNext()
}, [emblaApi])
return (
<div className="embla" ref={emblaRef}>
<div className="embla__container">
{product.gallery.map((_gallery) => (
<div key={_gallery.id}>
<NextImage media={_gallery.image} className="embla__slide" />
</div>
))}
</div>
<button
className="hidden md:inline embla__prev mr-2"
onClick={scrollPrev}
>
Prev
</button>
<button
className="hidden md:inline embla__next ml-2"
onClick={scrollNext}
>
Next
</button>
</div>
)
}
export default EmblaCarousel
The issue is
if (!props.media) {
return <NextImage {...props} />
}
in your custom Image component. When the media prop is falsy like undefined or null, you're passing everything else to NextImage but that everything else doesn’t include src prop which is mandatory for next Image component. Also your url extraction is dependent on media prop to be truthy and have a property called url. Can be seen from the next line :-
const { url, alternativeText } = props.media
And you intend to pass this url to src as can be seen from your usage. Either you can return null when media is falsy or you can filter out those items in your list where media prop is falsy and then map on it.
Not sure if you ever found an answer for this but I was running into the same issue and noticed that when looping through the multiple images object from Strapi the object is slightly different than with single images.
To fix this issue I supplied it to the getStrapiMedia() function in the same way it expects single images i.e:
{aboutpage?.attributes.shareCta.images.data.slice(0, 4).map((image) => (
<div key={image.id} className="relative h-64 w-full">
<Image
layout="fill"
objectFit="cover"
placeholder="blur"
blurDataURL={blurDataUrl}
src={
getStrapiMedia({ data: image }) ||
"/images/placeholders/image-placeholder.png"
}
/>
</div>
));
}
Hope this helps and kind regards
Replace NextImage with Image
import { getStrapiMedia } from "../utils/medias"
import Image from "next/image"
const NextImage = (props) => {
if (!props.media) {
return <Image {...props} />
}
const { url, alternativeText } = props.media
const loader = ({ src }) => {
return getStrapiMedia(src)
}
return (
<Image
loader={loader}
layout="responsive"
objectFit="contain"
width={props.media.width}
height={props.media.height}
src={url}
alt={alternativeText || ""}
/>
)
}
export default NextImage
Carousel.js
import React, { useCallback } from "react"
import useEmblaCarousel from "embla-carousel-react"
import NextImage from "./Image"
export const EmblaCarousel = (product) => {
const [emblaRef, emblaApi] = useEmblaCarousel()
useEmblaCarousel.globalOptions = { loop: true }
const scrollPrev = useCallback(() => {
if (emblaApi) emblaApi.scrollPrev()
}, [emblaApi])
const scrollNext = useCallback(() => {
if (emblaApi) emblaApi.scrollNext()
}, [emblaApi])
return (
<div className="embla" ref={emblaRef}>
<div className="embla__container">
{product.gallery.map((_gallery) => (
<div key={_gallery.id}>
<NextImage media={_gallery.image} className="embla__slide" />
</div>
))}
</div>
<button
className="hidden md:inline embla__prev mr-2"
onClick={scrollPrev}
>
Prev
</button>
<button
className="hidden md:inline embla__next ml-2"
onClick={scrollNext}
>
Next
</button>
</div>
)
}
export default EmblaCarousel
I am currently making a project over the database I created using Mock API. I created a button, created addToFavorites function. When the button was clicked, I wanted the selected product's information to go to the favorites, but I couldn't. I would be glad if you could help me on how to do this.
(Favorites.js empty now. I got angry and deleted all the codes because I couldn't.)
(
Recipes.js
import React, { useState, useEffect } from "react"
import axios from "axios"
import "./_recipe.scss"
import Card from "../Card"
function Recipes() {
const [recipes, setRecipes] = useState([])
const [favorites, setFavorites] = useState([])
useEffect(() => {
axios
.get("https://5fccb170603c0c0016487102.mockapi.io/api/recipes")
.then((res) => {
setRecipes(res.data)
})
.catch((err) => {
console.log(err)
})
}, [])
const addToFavorites = (recipes) => {
setFavorites([...favorites, recipes])
console.log("its work?")
}
return (
<div className="recipe">
<Card recipes={recipes} addToFavorites={addToFavorites} />
</div>
)
}
export default Recipes
Card.js
import React, { useState } from "react"
import { Link } from "react-router-dom"
import { BsClock, BsBook, BsPerson } from "react-icons/bs"
function Card({ recipes, addToFavorites }) {
const [searchTerm, setSearchTerm] = useState("")
return (
<>
<div className="recipe__search">
<input
type="text"
onChange={(event) => {
setSearchTerm(event.target.value)
}}
/>
</div>
<div className="recipe__list">
{recipes
.filter((recipes) => {
if (searchTerm === "") {
return recipes
} else if (
recipes.title.toLowerCase().includes(searchTerm.toLowerCase())
) {
return recipes
}
})
.map((recipe) => {
return (
<div key={recipe.id} className="recipe__card">
<img src={recipe.image} alt="foods" width={350} height={230} />
<h1 className="recipe__card__title">{recipe.title}</h1>
<h3 className="recipe__card__info">
<p className="recipe__card__info--icon">
<BsClock /> {recipe.time} <BsBook />{" "}
{recipe.ingredientsCount} <BsPerson />
{recipe.servings}
</p>
</h3>
<h3 className="recipe__card__desc">
{recipe.description.length < 100
? `${recipe.description}`
: `${recipe.description.substring(0, 120)}...`}
</h3>
<button type="button" className="recipe__card__cta">
<Link
to={{
pathname: `/recipes/${recipe.id}`,
state: { recipe }
}}
>
View Recipes
</Link>
</button>
<button onClick={() => addToFavorites(recipes)}>
Add to favorites
</button>
</div>
)
})}
</div>
</>
)
}
export default Card
Final Output:
I have implemented the addToFavorite() and removeFavorite() functionality, you can reuse it the way you want.
I have to do bit of modification to the code to demonstrate its working, but underlying functionality of addToFavorite() and removeFavotie() works exactly the way it should:
Here is the Card.js where these both functions are implemented:
import React, { useState } from "react";
import { BsClock, BsBook, BsPerson } from "react-icons/bs";
function Card({ recipes }) {
const [searchTerm, setSearchTerm] = useState("");
const [favorite, setFavorite] = useState([]); // <= this state holds the id's of all favorite reciepies
// following function handles the operation of adding fav recipes's id's
const addToFavorite = id => {
if (!favorite.includes(id)) setFavorite(favorite.concat(id));
console.log(id);
};
// this one does the exact opposite, it removes the favorite recipe id's
const removeFavorite = id => {
let index = favorite.indexOf(id);
console.log(index);
let temp = [...favorite.slice(0, index), ...favorite.slice(index + 1)];
setFavorite(temp);
};
// this variable holds the list of favorite recipes, we will use it to render all the fav ecipes
let findfavorite = recipes.filter(recipe => favorite.includes(recipe.id));
// filtered list of recipes
let filtered = recipes.filter(recipe => {
if (searchTerm === "") {
return recipe;
} else if (recipe.title.toLowerCase().includes(searchTerm.toLowerCase())) {
return recipe;
}
});
return (
<div className="main">
<div className="recipe__search">
<input
type="text"
onChange={event => {
setSearchTerm(event.target.value);
}}
/>
</div>
<div className="recipe-container">
<div className="recipe__list">
<h2>all recipes</h2>
{filtered.map(recipe => {
return (
<div key={recipe.id} className="recipe__card">
<img src={recipe.image} alt="foods" width={50} height={50} />
<h2 className="recipe__card__title">{recipe.title}</h2>
<h4 className="recipe__card__info">
<p>
<BsClock /> {recipe.time} <BsBook />{" "}
{recipe.ingredientsCount} <BsPerson />
{recipe.servings}
</p>
</h4>
<h4 className="recipe__card__desc">
{recipe.description.length < 100
? `${recipe.description}`
: `${recipe.description.substring(0, 120)}...`}
</h4>
<button onClick={() => addToFavorite(recipe.id)}>
add to favorite
</button>
</div>
);
})}
</div>
<div className="favorite__list">
<h2>favorite recipes</h2>
{findfavorite.map(recipe => {
return (
<div key={recipe.id} className="recipe__card">
<img src={recipe.image} alt="foods" width={50} height={50} />
<h2 className="recipe__card__title">{recipe.title}</h2>
<h4 className="recipe__card__info">
<p className="recipe__card__info--icon">
<BsClock /> {recipe.time} <BsBook />{" "}
{recipe.ingredientsCount} <BsPerson />
{recipe.servings}
</p>
</h4>
<h4 className="recipe__card__desc">
{recipe.description.length < 100
? `${recipe.description}`
: `${recipe.description.substring(0, 120)}...`}
</h4>
<button onClick={() => removeFavorite(recipe.id)}>
remove favorite
</button>
</div>
);
})}
</div>
</div>
</div>
);
}
export default Card;
Here is the live working app : stackblitz
You can get the previous favourites recipes and add the new ones.
const addToFavorites = (recipes) => {
setFavorites(prevFavourites => [...prevFavourites, recipes])
console.log("its work?")
}
I have the following Code, regarding a React Component:
<FilterGroups
data={usersList}
selectedGroups={selectedGroups}
onChange={this.onFilterSelect}
/>
The FilterGroups component is the following:
import React from 'react';
import PropTypes from 'prop-types';
import Icon from 'lenses/common/components/Icon';
import { flatten, uniq, map, pipe } from 'ramda';
import { Dropdown, Toggle, Menu } from 'shared/common/components/LensesDropdown';
class FilterGroups extends React.Component {
static propTypes = {
data: PropTypes.array,
selectedGroups: PropTypes.array,
onChange: PropTypes.func
};
static defaultProps = {
selectedGroups: []
};
renderBadge = () => {
const { selectedGroups } = this.props;
if (selectedGroups.length === 0) {
return null;
}
return <span className="badge badge-light ml-1">{selectedGroups.length}</span>;
};
onClick = groupName => e => {
const { onChange } = this.props;
e.preventDefault();
onChange(groupName);
};
getAllGroups = data =>
pipe(
map(user => user.groups),
uniq,
flatten
)(data);
render() {
const { data, selectedGroups } = this.props;
const allUniqueGroups = this.getAllGroups(data);
return (
<Dropdown>
<Toggle variant="primary">Filter by Group {this.renderBadge()}</Toggle>
<Menu>
{allUniqueGroups.map(group => (
<a
key={group}
className="dropdown-item d-flex justify-content-between align-items-center"
href="#"
onClick={this.onClick(group)}
>
<span className="ml-2">{group}</span>
{selectedGroups.includes(group) && <Icon icon="check" />}
</a>
))}
</Menu>
</Dropdown>
);
}
}
export default FilterGroups;
And I get the following error:Each child in an array or iterator should have a unique "key" prop.. Also this Check the render method ofFilterGroups.
I am adding a keyFiled={group}, inside the component, but I am getting nowhere. I am passing the group as prop as well. Can someone tell me what am I doing wrong here?
Try this:
<a
key={group}
className="dropdown-item d-flex justify-content-between align-items-center"
href="#"
onClick={this.onClick(group)}
>
<span key={group} className="ml-2">{group}</span>
{selectedGroups.includes(group) && <Icon key={group} icon="check" />}
</a>
))}
Insert key into span and Icon.
Just give it an index to be sure they are unique.
allUniqueGroups.map((group, index) and index+group as a key.
The first thing Each child in an array or iterator should have a unique "key" prop is a warning not an error which means that you have another log on your console that shows the error I believe, it could be that allUniqueGroups is undefined and the solution for this is: Note (allUniqueGroups && at the beginning of the code.
{allUniqueGroups && allUniqueGroups.map(group => (
<a
key={group}
className="dropdown-item d-flex justify-content-between align-items-center"
href="#"
onClick={this.onClick(group)}
>
<span className="ml-2">{group}</span>
{selectedGroups.includes(group) && <Icon icon="check" />}
</a>
))}