Best way to create a search bar with image in results - html

I want to create a search bar with a list of results including the image and description of the product, I've tried with a datalist component but the html option tag does not allow any other element than text. This component takes the user input, fetch the data to the api and renders a list with the results.
This is my code:
import React, { useEffect, useState } from 'react'
import api from '../../utils/api'
import { baseUrl } from '../../utils/constants'
import styles from './searchBar.module.css'
const SearchBar = () => {
const [search, setSearch] = useState('')
const [results, setResults] = useState([])
useEffect(() => {
const getResults = async () => {
if (search.length > 2) {
try {
const response = await api.post('p/ver-productos', {
cuantos: 10,
filtros: {
nombre: [search],
},
})
if (
response.data.datos.items &&
response.data.datos.items.length > 0
) {
setResults(response.data.datos.items)
} else {
setResults([])
}
} catch (error) {
console.error(error)
setResults([])
}
}
}
getResults()
}, [search])
return (
<>
<input
className={styles.toolbarInput}
placeholder='Buscar productos'
list='results'
value={search}
onChange={e => setSearch(e.target.value)}
/>
<datalist id='results'>
{results.length > 0 &&
results.map((result: any) => {
return (
<option key={result.id}>
<img
src={baseUrl + result.multimedia[0].url}
alt={result.nombre}
/>
{result.nombre}
</option>
)
})}
</datalist>
</>
)
}
export default SearchBar

Related

How to limit the number of options in a select?

I am working on a project using React and tailwind.
I would like to filter the options I mean I want see to at most 3 options. I tried slice but it is not a solution because using slice for instance if I type a I want to see at most 3 words which contains the letter a if I type b I want to see at most 3 words which contains the letter b and that words for a and b can be different so slice cannot be a solution.
Here is my code :
import React, { Component } from "react";
import Select, { components} from "react-select";
import { useState } from "react";
let cheeses = ["Wagasi", "Kalari", "Halloumi", "Manouri"];
let options = [];
options = options.concat(cheeses.map((x) => "Cheese - " + x));
const Foo = () => {
const [value, setValue] = useState("");
function MakeOption(x) {
if (value) {
return { value: x, label: x };
} else {
return { value: "", label: "" };
}
}
const handleInputChange = (value, e) => {
if (e.action === "input-change") {
setValue(value);
}
};
const Input = props => <components.Input {...props} maxLength={5} />;
return (
<Select
isMulti
name="colors"
options={options.map((x) => MakeOption(x)).filter(opt => opt.value !== "")}
className="basic-multi-select"
classNamePrefix="select"
closeMenuOnSelect={false}
onInputChange={handleInputChange}
inputValue={value}
noOptionsMessage={() => null}
/>
);
};
export default Foo;
Could you help me please ?
I think this code works like you want.
The problem have been solved with a second variable for the select options.
import React, { Component } from "react";
import Select, { components} from "react-select";
import { useState } from "react";
let cheeses = ["Wagasi", "Kalari", "Halloumi", "Manouri"];
let options = [];
options = options.concat(cheeses.map((x) => "Cheese - " + x));
const Foo = () => {
const [value, setValue] = useState("");
const [optionsToShow, setOptionsToShow] = useState([]);
function MakeOption(x) {
return { value: x, label: x };
}
const handleInputChange = (value, e) => {
if (e.action === "input-change") {
setValue(value);
const nextOptions = value ? options.map((x) => MakeOption(x)).filter((opt) => opt.label.toLowerCase().includes(value.toLowerCase())) : [];
setOptionsToShow(nextOptions.length > 3 ? nextOptions.splice(1,3) : nextOptions);
}
};
const Input = props => <components.Input {...props} maxLength={5} />;
return (
<Select
isMulti
name="colors"
options={optionsToShow}
className="basic-multi-select"
classNamePrefix="select"
closeMenuOnSelect={false}
onInputChange={handleInputChange}
inputValue={value}
noOptionsMessage={() => null}
/>
);
}
export default Foo;
I hope I've helped you

React : i have following code, i am using setInterval to change text value time to time ,when i switch pages and come back text is changing reallyfast

I assume when component is rendered multiple times something happens to setinterval,but how can i fix this.
bottom code is for Store that i am using and i don't understand.someone said that i must have useffect outside component but then it gives me error.
Anyways im new to react so i need help ,everyones appriciated.Thanks.
import SmallLogo from '../img/logo.svg';
import StarskyText from '../img/starskyproject.svg';
import './Statement.css'
import { BrowserRouter as Router,Routes,Route,Link } from "react-router-dom";
import { getElementError } from '#testing-library/react';
import react, { useRef , useState, useEffect } from 'react';
import { useDispatch, useSelector } from "react-redux";
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
import { store } from "./appReducer";
function TempText(props) {
return <span className="yellow changetext"> {props.body} </span>;
}
function doUpdate(callback) {
setInterval(callback, 1300);
}
export default function Statement(){
const dispatch = useDispatch();
const textOptions = ["NFT", "CRYPTO", "METAVERSE", "WEB3"];
const tempText = useSelector((state) => state.tempText);
function change() {
let state = store.getState();
const index = state.index;
console.log(index);
console.log(textOptions[index]);
dispatch({
type: "updatetext",
payload: textOptions[index]
});
let newIndex = index + 1 >= textOptions.length ? 0 : index + 1;
dispatch({
type: "updateindex",
payload: newIndex
});
}
useEffect(() => {
doUpdate(change);
}, []);
var [dropdownOpen , Setdrop] = useState(false);
return(
<div>
<Link to="/">
<img className='star-fixed' alt='starlogo' src={SmallLogo}></img>
</Link>
<img className='starsky-fixed' alt='starsky-project' src={StarskyText}></img>
<div className='text-content'>
<span className='statement-text'>WEB3 IS NOT ONLY THE FUTURE.
IT’S THE ONLY FUTURE!</span>
<span className='starsk-link'>starsk.pro</span>
</div>
<div className='text-content-bottom'>
<span className='statement-text-bottom'>CREATE YOUR NEXT
<TempText body={tempText} />
<span className='flex'> PROJECT WITH
<Dropdown className="hover-drop-out" onMouseOver={() => Setdrop(dropdownOpen=true) } onMouseLeave={() => Setdrop(dropdownOpen=false)} isOpen={dropdownOpen} toggle={() => Setdrop(dropdownOpen = !dropdownOpen) }>
<DropdownToggle className='hover-drop'> STRSK.PRO </DropdownToggle>
<DropdownMenu> </DropdownMenu>
</Dropdown> </span>
</span>
</div>
</div>
)
}
import { createStore } from "redux";
const initialState = {
tempText: "NFT",
index: 1
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case "updatetext":
return {
...state,
tempText: action.payload
};
case "updateindex":
return {
...state,
index: action.payload
};
default:
return state;
}
};
export const store = createStore(reducer);
You can clear your timer by calling clearTimeout function with a reference to your timer when your component unmounting.
useEffect(() => {
const timer = setInterval(change, 1300);
// in order to clear your timeout
return () => clearTimeout(timer);
}, [])

How to have button's onClick override as={Link} to={} (anchor tag)?

I have a button inside my <List.Item as={Link} to={/lists/${list.id}}> which I want to activate but the as={Link} has converted the entire greyed selection an anchor tag; this makes it so, when I click the button, it just links me to the route rather than executing onClick delete functionality. How can I maintain the onClick delete functionality of the <DeleteButton> but still allow for the rest of the List.item (greyed section) to keep link functionality ?
Profile.js (List component in photo above)
import React, { useContext } from "react";
import { useQuery } from "#apollo/react-hooks";
import { List, Image } from "semantic-ui-react";
import { Link } from "react-router-dom";
import ListForm from "../components/ListForm";
import DeleteButton from "../components/DeleteButton";
import { AuthContext } from "../context/auth";
// import ListForm from "../components/ListForm";
import { FETCH_LISTS_QUERY } from "../util/graphql";
import "../RankList.css";
function Profile(props) {
const { user } = useContext(AuthContext);
let lists = "";
const { loading, data, error } = useQuery(FETCH_LISTS_QUERY);
console.log(error);
console.log(`Loading: ${loading}`);
console.log(data);
if (data) {
lists = { data: data.getLists.filter((l) => l.username === user.username) };
console.log(lists);
}
// function deleteListCallback() {
// props.history.push("/");
// }
return (
<List selection verticalAlign="middle">
{user && <ListForm />}
{lists.data &&
lists.data.map((list) => (
<List.Item as={Link} to={`/lists/${list.id}`}>
<Image avatar src="/images/avatar/small/helen.jpg" />
<List.Content>
<List.Header>{list.title}</List.Header>
</List.Content>
<DeleteButton listId={list.id} />
</List.Item>
))}
</List>
);
}
export default Profile;
DeleteButton.js (Delete button component in photo above)
import React, { useState } from "react";
import gql from "graphql-tag";
import { useMutation } from "#apollo/react-hooks";
import { Button, Confirm, Icon } from "semantic-ui-react";
import { FETCH_LISTS_QUERY } from "../util/graphql";
import MyPopup from "../util/MyPopup";
function DeleteButton({ listId, listItemId, commentId, callback }) {
const [confirmOpen, setConfirmOpen] = useState(false);
let mutation;
if (listItemId) {
mutation = DELETE_LIST_ITEM_MUTATION
} else if (commentId) {
mutation = DELETE_COMMENT_MUTATION
} else {
mutation = DELETE_LIST_MUTATION
}
// const mutation = commentId ? DELETE_COMMENT_MUTATION : DELETE_LIST_MUTATION;
const [deleteListOrComment] = useMutation(mutation, {
update(proxy) {
setConfirmOpen(false);
// remove list from cache
if (!commentId && !listItemId) {
const data = proxy.readQuery({
query: FETCH_LISTS_QUERY,
});
const resLists = data.getLists.filter((p) => p.id !== listId);
proxy.writeQuery({
query: FETCH_LISTS_QUERY,
data: { getLists: [...resLists] },
});
}
if (callback) callback();
},
variables: {
listId,
commentId,
},
onError(err) {
console.log(err.graphQLErrors[0].extensions.exception.errors);
},
});
return (
<>
<MyPopup content={commentId ? "Delete comment" : "Delete list"}>
<Button
as="div"
color="red"
floated="right"
onClick={() => setConfirmOpen(true)}
>
<Icon name="trash" style={{ margin: 0 }} />
</Button>
</MyPopup>
<Confirm
open={confirmOpen}
onCancel={() => setConfirmOpen(false)}
onConfirm={deleteListOrComment}
/>
</>
);
}
const DELETE_LIST_MUTATION = gql`
mutation deleteList($listId: ID!) {
deleteList(listId: $listId)
}
`;
const DELETE_LIST_ITEM_MUTATION = gql`
mutation deleteListItem($listId: ID!, $listItemId: ID!) {
deleteListItem(listId: $listId, listItemId: $listItemId) {
id
comments {
id
username
createdAt
body
}
commentCount
}
}
`;
const DELETE_COMMENT_MUTATION = gql`
mutation deleteComment($listId: ID!, $commentId: ID!) {
deleteComment(listId: $listId, commentId: $commentId) {
id
comments {
id
username
createdAt
body
}
commentCount
}
}
`;
export default DeleteButton;
You can add preventDefualt (and possibly stopPropagation if necessary) to the event passed to the button's onClick handler.
<Button
as="div"
color="red"
floated="right"
onClick={e => {
e.preventDefualt();
setConfirmOpen(true);
}}
>
One solution could be to create a custom link component and redirect with the push method of the history object. With this method you can add a ref to your DeleteButton and check if the event target isn't the DeleteButton component, then redirect. I don't think it's the cleanest solution, I made a little sandbox

Render input fields dynamically inside a list

I have set of components where it would consist of input fields along with text rows.
As given in the image the users should be able to add categories and description. After adding them they will be rendered as a list of components. like this
Inside a category there will be tags as given in the above image and to add them i have to add a input component. This input component should be available only when the user clicks on the Add tag button below each category row. When a user clicks on it,it should enable the input(should render a input component inside the selected category row) and should be able to type the tag name on it and save it. I need to make this input field enable only when i click on the add tag button. and it should enable only in the selected category row. This is the code that i have tried.
import React, { Component, Fragment } from "react";
import { Button, Header, Input } from "semantic-ui-react";
import "semantic-ui-css/semantic.min.css";
import ReactDOM from "react-dom";
class App extends Component {
state = {
category: "",
description: "",
categories: []
};
onChange = (e, { name, value }) => {
this.setState({ [name]: value });
};
addCategory = () => {
let { category, description } = this.state;
this.setState(prevState => ({
categories: [
...prevState.categories,
{
id: Math.random(),
title: category,
description: description,
tags: []
}
]
}));
};
addTag = id => {
let { tag, categories } = this.state;
let category = categories.find(cat => cat.id === id);
let index = categories.findIndex(cat => cat.id === id);
category.tags = [...category.tags, { name: tag }];
this.setState({
categories: [
...categories.slice(0, index),
category,
...categories.slice(++index)
]
});
};
onKeyDown = e => {
if (e.key === "Enter" && !e.shiftKey) {
console.log(e.target.value);
}
};
tags = tags => {
if (tags && tags.length > 0) {
return tags.map((tag, i) => {
return <Header key={i}>{tag.name}</Header>;
});
}
};
enableTagIn = id => {};
categories = () => {
let { categories } = this.state;
return categories.map(cat => {
return (
<Fragment key={cat.id}>
<Header>
<p>
{cat.title}
<br />
{cat.description}
</p>
</Header>
<Input
name="tag"
onKeyDown={e => {
this.onKeyDown(e);
}}
onChange={this.onChange}
/>
<Button
onClick={e => {
this.addTag(cat.id);
}}
>
Add
</Button>
{this.tags(cat.tags)}
</Fragment>
);
});
};
render() {
return (
<Fragment>
{this.categories()}
<div>
<Input name="category" onChange={this.onChange} />
<Input name="description" onChange={this.onChange} />
<Button onClick={this.addCategory}>Save</Button>
</div>
</Fragment>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
This is the codesandbox url.
Any idea on how to achieve this?.
I changed your code by using function components and react hooks and i created category component which has it own state like this:
import React, { Fragment } from "react";
import { Button, Header, Input } from "semantic-ui-react";
import "semantic-ui-css/semantic.min.css";
import ReactDOM from "react-dom";
const App = () => {
const [Category, setCategory] = React.useState({
title: "",
description: ""
});
const [Categories, setCategories] = React.useState([]);
return (
<div>
{console.log(Categories)}
<Input
value={Category.title}
onChange={e => setCategory({ ...Category, title: e.target.value })}
/>
<Input
value={Category.description}
onChange={e =>
setCategory({ ...Category, description: e.target.value })
}
/>
<Button onClick={() => setCategories([...Categories, Category])}>
Save
</Button>
<div>
{Categories.length > 0
? Categories.map(cat => <CategoryItem cat={cat} />)
: null}
</div>
</div>
);
};
const CategoryItem = ({ cat }) => {
const [value, setvalue] = React.useState("");
const [tag, addtag] = React.useState([]);
const [clicked, setclicked] = React.useState(false);
const add = () => {
setclicked(false);
addtag([...tag, value]);
};
return (
<Fragment>
<Header>
<p>
{cat.title}
<br />
{cat.description}
</p>
</Header>
<Input
name="tag"
value={value}
style={{ display: clicked ? "initial" : "none" }}
onChange={e => setvalue(e.target.value)}
/>
<Button onClick={() => (clicked ? add() : setclicked(true))}>Add</Button>
<div>{tag.length > 0 ? tag.map(tagname => <p>{tagname}</p>) : null}</div>
</Fragment>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
and here a sandbox

Redux loses state when navigating to another page using react-router 'history.push'

(as you can see my reputation is not very high :) and I understand that if you don't like my question it is going to be my last one, therefore I am going to write it as good as I can :)
The problem I am facing is a similar to:
Redux loses state when navigating to another page
However, the answer to the above question was to use 'history.push', which is what I am doing, and I am still having a problem.
I am using:
"react": "^16.0.0"
"react-redux": "^5.0.6"
"react-router": "^4.2.0"
"react-router-dom": "^4.2.2"
"redux": "^3.7.2"
"redux-promise":"^0.5.3"
"axios": "^0.17.1"
I am doing the following:
In a react component, "SearchText", getting a text string and calling an action creator
In the action creator, using the text string to send an HTTP request to goodreads.com
In my reducer, using the action payload to set the redux state
Using another component, "BookResults" (in another route), to display this state
The component "SearchText" has a link to the "BookResults" page.
So, once "SearchText" fires the action creator, if (when I see on the console that a result is received and the state is set with a list of books) I click on the link that routes to "BookResults", I see the list of books.
If, however, "SearchText" uses (when firing the action creator) a callback that performs history.push of the new page, and this callback is called by 'axios(xxx).then', the state is not set properly, although I see in the console that the HTTP request was successful.
I am sure you can see what I am doing wrong (and I hope it is not very stupid)... Please tell me.
Here is the code:
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { createStore, applyMiddleware } from 'redux';
import ReduxPromise from 'redux-promise';
import SearchText from './components/search_text';
import BookResults from './components/book_results';
import reducers from './reducers';
const createStoreWithMiddleware = applyMiddleware(ReduxPromise)(createStore);
ReactDOM.render(
<Provider store={createStoreWithMiddleware(reducers)}>
<BrowserRouter>
<div>
<BrowserRouter>
<Switch>
<Route path="/book_results" component={BookResults} />
<Route path="/" component={SearchText} />
</Switch>
</BrowserRouter>
</div>
</BrowserRouter>
</Provider>
, document.querySelector('#root'));
SearchText component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Link } from 'react-router-dom';
import { searchForBooks } from '../actions';
class SearchText extends Component {
constructor(props) {
super(props);
this.state = {
searchText: ''
};
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.handleSearchTextChange = this.handleSearchTextChange.bind(this);
}
handleSearchTextChange(e) {
this.setState({ searchText: e.target.value });
}
handleFormSubmit(e) {
e.preventDefault();
const formPayload = {
searchText: this.state.searchText
};
console.log("In SearchBooks/handleFormSubmit. Submitting. state: ", this.state);
this.props.searchForBooks(formPayload, () => {
this.props.history.push(`/book_results`);
});
}
render() {
return (
<form className="container" onSubmit={this.handleFormSubmit}>
<h3>Search Form</h3>
<div className="form-group">
<label className="form-label">{'Search Text:'}</label>
<input
className='form-input'
type='text'
name='searchText'
value={this.state.searchText}
onChange={this.handleSearchTextChange}
onBlur={this.handleSearchTextBlur}
placeholder='' />
</div>
<br />
<input
type="submit"
className="btn btn-primary float-right"
value="Submit"/>
<br /><br />
<Link to={`/book_results`}>⇐ Book Results</Link>
</form>
);
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ searchForBooks: searchForBooks }, dispatch);
}
export default connect(null, mapDispatchToProps)(SearchText);
BookResults component
import React from 'react';
import { connect } from 'react-redux';
import _ from 'lodash';
import Book from './book';
class BookResults extends React.Component {
render() {
let books;
const booksArray = _.values(this.props.bookResults);
console.log("***In BookResults. booksArray: ", booksArray);
if (booksArray.length === 0) {
books = "No books to display";
} else {
books = booksArray.map( (book) => {
return (
<Book book={book} key={book.id} />
);
});
}
return (
<div>
<h2>Search Results</h2>
<br />
<ul>
{books}
</ul>
</div>
);
}
}
function mapStateToProps(state) {
return {
bookResults: state.bookResults,
cats: state.cats
};
}
export default connect(mapStateToProps)(BookResults);
Book component
import React from 'react';
const Book = (props) => (
<li>
{props.book.title}
</li>
);
export default Book;
actions/index.js
As you can see below, the following line is commented out:
// .then(() => callback());
If I include it, I have the problem.
import axios from 'axios';
export const SEARCH_FOR_BOOKS = 'search_for_books';
const GOODREADS = "https://www.goodreads.com/search/index.xml";
const KEY = "xxx";
export function searchForBooks(values, callback) {
let result;
console.log("In actions/searchForBooks. values: ", values);
if (!values.searchText || values.searchText === "") {
console.error("*** ERROR *** In actions/searchForBooks." +
"values.searchText: ", values.searchText);
} else {
const searchUrl = `${GOODREADS}?key=${KEY}&q=${values.searchText}`;
console.log("In actions/searchForBooks. url: " + searchUrl);
result = axios.get(searchUrl);
// .then(() => callback());
}
return {
type: SEARCH_FOR_BOOKS,
payload: result
};
}
reducers/index.js
import { combineReducers } from 'redux';
import bookResultsReducer from './reducer_book_results';
const rootReducer = combineReducers({
bookResults: bookResultsReducer
});
export default rootReducer;
The reducer
import { parseString } from 'xml2js';
import _ from 'lodash';
import { SEARCH_FOR_BOOKS } from '../actions/index';
const bookResults = {};
export default function bookResultsReducer(state = bookResults, action) {
switch (action.type) {
case SEARCH_FOR_BOOKS:
console.log("In bookResultsReducer. payload: ", action.payload);
if (action.error) { // error from goodreads search books
console.error("*** APP ERROR *** In bookResultsReducer. action.error: ", action.error);
} else if (!action.payload || !action.payload.data) {
console.error("*** APP ERROR *** In bookResultsReducer." +
" action.payload or action.payload.data is undefined", action.payload);
} else {
parseString(action.payload.data, function(err, result) {
if (err) {
console.error("*** APP ERROR *** In bookResultsReducer. Error from parseString: ", err);
} else {
state = Object.assign({}, getBooks(result));
}
});
}
console.log("In bookResultsReducer. new state: ", state);
return state;
break;
default:
return state;
}
}
function getBooks(data) {
const bookResults = data.GoodreadsResponse.search[0].results[0].work;
if (!bookResults || bookResults.length === 0) {
return {};
} else {
const results = bookResults.map( (book, index) => {
const bookInfo = book.best_book[0];
return (
{ id: index + 1,
title: bookInfo.title[0] }
);
});
return _.mapKeys(results, 'id');
}
}
Someone sent me the solution by mail.
The error was in the actions/index.js file.
Instead of:
import axios from 'axios';
export const SEARCH_FOR_BOOKS = 'search_for_books';
const GOODREADS = "https://www.goodreads.com/search/index.xml";
const KEY = "xxx";
export function searchForBooks(values, callback) {
let result;
console.log("In actions/searchForBooks. values: ", values);
if (!values.searchText || values.searchText === "") {
console.error("*** ERROR *** In actions/searchForBooks." +
"values.searchText: ", values.searchText);
} else {
const searchUrl = `${GOODREADS}?key=${KEY}&q=${values.searchText}`;
console.log("In actions/searchForBooks. url: " + searchUrl);
result = axios.get(searchUrl)
.then(() => callback());
}
return {
type: SEARCH_FOR_BOOKS,
payload: result
};
}
I should have written:
import axios from 'axios';
export const SEARCH_FOR_BOOKS = 'search_for_books';
const GOODREADS = "https://www.goodreads.com/search/index.xml";
const KEY = "xxx";
export function searchForBooks(values, callback) {
let result;
console.log("In actions/searchForBooks. values: ", values);
if (!values.searchText || values.searchText === "") {
console.error("*** ERROR *** In actions/searchForBooks." +
"values.searchText: ", values.searchText);
} else {
const searchUrl = `${GOODREADS}?key=${KEY}&q=${values.searchText}`;
console.log("In actions/searchForBooks. url: " + searchUrl);
result = axios.get(searchUrl)
.then((res) => {
callback();
return res;
});
}
return {
type: SEARCH_FOR_BOOKS,
payload: result
};
}
Explanation:
The issue is that the returned value from axios.get is passed to the .then clause, and whatever is returned from the .then clause is set to be the value of result.
My error was that I didn't return anything from the .then clause, and therefore the value of result was undefined, and not the returned promise.