Implement filter options in Gatsby - javascript

I'm fairly new to Gatsby and React and I couldn't find an answer to my problem.
I want to be able to have filtering options on the home of my website that allow the user to display only content relevant to that filter.
Let's say my posts are recipes and they are categorized by type: main, snack and dessert.
I want a filter, can be a button or a drop-down it doesn't matter, and when the user selects it I will display only the items relevant. Ideally I'm going to have multiple filters, around 4-5 for different properties of the frontmatter of my posts .
From what I understand it's not really something I can do with graphql because after build I cannot access it anymore so I was seeking some advice from more experienced devs.

If you have a small set of posts, I think you can make a search component that get all the posts & then use something like js-search or flexsearch to index them.
In a non-page component (not in src/pages folder), you can use StaticQuery to get all the posts' info.
Say you have this graphql query result:
data: {
allMarkdownRemark: {
edges: [{
node: {
id: '1234-1233...',
fields: {
slug: '/hello/'
},
frontmatter: {
title: 'hello',
tags: [ 'go', 'js' ]
}
}
}, {
node: {
id: ...
}
}]
}
}
Then you can index & search for post, say, with js-search:
const posts = data.allMarkdownRemark.edges.map(({ node }) => node) // unwrap edges
const postIndex = new JsSearch.Search('id')
postIndex.addIndex(['frontmatter', 'tags']) // index node.frontmatter.tags
postIndex.addIndex(['frontmatter', 'title'])
postIndex.addDocuments(posts) // add data to array
const results = postIndex.search('go')
console.log(results) // [{ id: '1234-1233...', frontmatter: { title: 'hello', tags: [ 'go', 'js' ]}}]
Then you can store this result in, say, the component's state & render the results as posts.
Gatsby's doc also has a guide on adding search to your site, though I think the js-search part is a bit overwhelming.

Related

Handling updates on nested screens using Context and React Navigation in React Native

Let's say I am building a shopping list app. I have the ability to create different shopping lists. I have three screens:
-- Shopping Lists: displays all the lists
---- List Details: displays all the items from a list
------ List Item Details: displays all the info about an item
To store the state and avoid prop drilling I use Context. My state could look like:
shoppingLists = [
{
title: 'Groceries',
items: [{
name: 'Apples',
quantity: 3
... (other info)
},
items: [{
name: 'Oranges',
quantity: 6,
...
}]
},
{
title: 'Office Supplies',
items: [{
name: 'Paper',
quantity: 2
... (other info)
},
items: [{
name: 'Pens',
quantity: 25,
...
}]
}]
When I tap on a Shopping List on the Shopping Lists screen I do
navigation.navigate('ListDetails', {params: listItem})
In the List Details screen I have de ability to change the quantity of an item or delete them. If I do any of this actions I have to make an API call to my server to update the value on the database. Here are my questions:
Currently I store the values in a local variable in my screen for example:
const [title, setTitle] = useState(props.route.params.title)
const [items, setItems] = useState(prop.route.params.items)
And if I make a change in the quantity I use setState to update my local array and then make an API call to update the context. This results problematic when adding more nested screens.
For example if I would give the user the ability to change the item quantity in the List Item Details screen, when the user goes back to the List Details screen, the values would not be updated.
My question is, which is the correct way to grab the state of the context and update it locally?
For example should I do something like:
navigation.navigate('ListDetails', {params: { listName: 'Groceries' })
And then in my List Details screen grab the correct list from the array like:
const {lists} = useContext(ShoppingLists)
const list = lists.filter(l => l.title === props.route.params.title)
What is the correct way?
The other question I have is a more general question. I want to use optimistic responses: when the user updates the quantity of an item in the list I update it locally and send the request to the server. If there's an error rollback the change. Which would be the correct way of doing this?
Thanks!
I would advise against updating context data in such a way. You could create a setList() method in the context instead, or updateList() to aggregate new data to the existing data.
This is related to the previous one. You could wrap the logic of POSTing to the server in a try-catch block and throw an error when it's not successful. Then you can set the new value only when it's successful. Another option would be to return the promise, so you could handle errors from outside, like setList().then(response => {}).catch(error => {}) though I don't think this looks that good.
Think you're over complicating it and also in your list you're not showing an id for each item. You should build your database with all the items and their unique id then call upon that when needed. The id should never change and that's easier to pass around and reference.
For example you can build an object in context with just the id and quantity and use useReducer for this:
https://reactjs.org/docs/hooks-reference.html#usereducer

Get user's events with Firebase (many to many relationship)

I'm a Front-End Developer and for the first-time, I'm using Firebase to build an application. I read some documentation/articles and watched this interesting video about foreign keys (many to many relationship): https://www.youtube.com/watch?v=ran_Ylug7AE&list=PLl-K7zZEsYLlP-k-RKFa7RyNPa9_wCH2s&index=2.
I'm trying to apply this approach to retrieve events for a specific user. Ideally, I want to get this information without requesting/paying for too much data. First, this is how I set-up the following database structure (see video):
{
users:
"abc": {
firstname: "Maxime",
...
},
"def": {
firstname: "John",
...
},
},
events: {
"eventAbc-": {
title: "Some great event",
...
},
"eventDef": {
title: "Another great event",
...
}
},
eventAttendees: {
"eventAbc": {
abc: true,
def: true,
},
"eventDef": {
abc: true,
}
}
}
To get user's events, I have the following which actually works:
getEvents(userId) {
const self = this;
const query = firebase.firestore().collection('eventAttendees');
var promises = [];
query.onSnapshot(function(snap) {
snap.docs.forEach(doc => {
const data = doc.data();
if (data[userId]) {
// user has been invited to this event
promises.push(self.getEvent(doc.id));
}
});
Promise.all(promises).then((results) => {
console.log("All events");
console.log(results);
});
});
}
getEvent(eventId) {
return new Promise((resolve) => {
const query = firebase.firestore()
.collection('events')
.doc(eventId);
query.onSnapshot(function(snap) {
resolve(snap.data());
});
});
}
getEvents('abc');
Questions
I don't think/know if I have the right and optimized approach? Any documentation/Github project I should look into as reference?
What does happen if 'abc' has been invited to 1 million events? I feel, I'm looping and need to handle pagination out of the box. Is there a better way?
Let's assume, each event has a lot of information (details). However, on the homepage, I just need to display main information (event title, event date). Where should I store that to avoid loading a lot of information.
Hopefully, there is someone who can find the time to reply to my questions.
Thanks.
You seem to be watching David's great video series Firebase for SQL developers, which was written for the Firebase Realtime Database. Since your code uses the Cloud Firestore API, I'd recommend switching over to Todd's Getting to know Cloud Firestore series, which contains similar information and much more, but then tailored towards Firestore.
What does happen if 'abc' has been invited to 1 million events?
No user is ever going to watch those million events. So don't load them. Think of how many items a user will realistically see, and then load those. Typically this will be one or a few screenfuls. If you think they may want to load more, allow them to load a new page. Search firebase and pagination to see many questions about that topic.
Let's assume, each event has a lot of information (details). However, on the homepage, I just need to display main information (event title, event date). Where should I store that to avoid loading a lot of information.
You should only load the data you need. If you need a subset of the data for each event in the initial list view, create a separate node or collection with just that information for each event. Yes, you're indeed duplicating data that way, but that is quite normal in NoSQL databases.
Thanks for taking the time to reply to my questions. Indeed, I like this video series and I'm looking forward to watch the one you've recommended which seems to be more recent.
I agree with you and definitely want to load only what's needed. At the moment, I don't have much datas so it's difficult for me to conceive/anticipate that. I should have probably taken advantage of .limit(10) or .startAt() in the function getEvents?
I'm not familiar with database in general and don't really know where I should have a subset of the data for each event? Is the following what you would call initial list view? For my example, is it the right place? If not, where would put that?
eventAttendees: {
"eventAbc": {
title: "Some great event",
eventDate: "",
moreKeyInformation: "",
abc: true,
def: true,
},
"eventDef": {
title: "Another great event",
eventDate: "",
moreKeyInformation: "",
abc: true,
}
}

Using Sequelize to find all images by related keyword with an array of keyword ID values to include / exclude

I have a Node.js/Express/Sequelize project where I'm storing a library of images with related keywords, and I want to be able to search for images by those keywords.
I have an images table with a many-to-many relationship to keywords, and a keywords table with a many-to-many relationship to images.
Image.belongsToMany(Keywords, {
as: 'keywords',
through: {
model: ImageKeywords,
unique: false
},
foreignKey: 'image_id',
constraints: false
});
Keywords.belongsToMany(Image, {
as: 'image',
through: {
model: ImageKeywords,
unique: false
},
foreignKey: 'keyword_id',
constraints: false
});
I have an array of keyword IDs to search with using AND, and an array of keywords to exclude from the search using NOT. These are entered by the user via a search form, but essentially the following demonstrates the structure.
let keywordsAnd = [1,2,3]
let keywordsNot = [4,5,6]
For each image, I've associated one or many keywords through the join table, and can fetch images and display all of their keywords without any difficulty.
What I'm trying to accomplish is when a user enters a list of keywords they want to search for, and specifies some to exclude, I want to find all of the images using an AND search with the keywordsAnd array, and exclude those in the keywordsNot array.
For the above example, I want to return any image associated with all of the keywords with the IDs 1, 2 or 3, and excludes any of those images associated any of the keywords of ID 4, 5 or 6.
I was trying to use the following via the Keywords.findAll path:
Keywords.findAll({
include: [{
model: Image,
as: 'image',
include: [{
model: Keywords,
as: 'keywords',
attributes: ['id', 'name']
}]
}],
where: {
id: {
[Op.and]: [
keywordsAnd,
{ [Op.not]: keywordsNot }
]
}
}
})
This appears to do an OR search using the keywordsAnd array, and throws an error if keywordsNot is defined.
So with the above [1,2,3] array, I get all the images with the keyword IDs 1, 2 or 3.
It occurs to me that perhaps I should be searching through Image.findAll, including the Keywords model, but I'm not familiar enough with the mechanics of Sequelize at this point to know the correct approach and syntax, so any guidance would be appreciated.
First of all, you can use Image.findAll instead Keywords.findAll as first query and within use include in order to create a join with Keywords in SQL query level.
Inside include you can add a where clause as you can see above in order to alter Keywords research.
Finally you can use Op.in and Op.notIn, both of them are used to work with arrays.
See Sequelize documentation and put special attention to Relations / Associations and Operators sections.
I hope it helps!
Image.findAll({
include: [ {
model: Keywords,
as: 'Keywords',
attributes: ['id', 'name'],
where: {
id: {
[Op.and]: [
{ [Op.in]: keywordsAnd },
{ [Op.notIn]: keywordsNot }
]
}
}
} ]
});

Implementing javascript / json objects in ReactJS

I want to create some kind of temporary "database" for my test project.
What is the best solution to store those objects inside React app and how to access it in different components?
To clear what I mean, I've got a search bar, after enter is pressed the function would go through the "database" and if database.title is equal to user input a new div would be rendered with object details after a button is clicked.
I would be grateful for pointing me in the right direction.
You can create a dummy data file containing an array or some sort of data in it and then import it in which component you want to test.
Example:
// dummyData.js
const dummyData = [
{
title: 'this is a title',
type: 'book'
},
{
title: 'this is another title',
type: 'movie'
},
{
title: 'this is a foo',
type: 'bar'
}
];
export default dummyData;
// on your component
import dummyData from '/path/to/your/dummyData.js';

How to hydrate Project name in SnapshotStore

When using the SnapshotStore, I have been unable to hydrate the "Name" property of the objects that are returning as part of my query. I get the correct OID for the project, but I would like to display the string name of the project instead of the OID. How can I go about doing that?
This is the code that I am using to query, but when I add the "Project" property to the Hydrate field, it does not seem to matter. If I comment out the hydrate line entirely, the state and resolution come back as unhydrated and not readable (by most people) so I know that it is at least working.
doSearch: function(query, fields, sort, pageSize, callback){
var transformStore = Ext.create('Rally.data.lookback.SnapshotStore', {
context: {
workspace: this.context.getWorkspace(),
project: this.context.getProject()
},
fetch: fields,
find: query,
autoLoad: true,
hydrate: ["State","Resolution","Project"],
listeners: {
scope: this,
load: this.processSnapshots
}
});
},
"We could not find the user-friendly form for the following: 'Project':7579240995" - is what I get when trying to include "Project" in the hydrate field.
I read somewhere that hydrate only works with drop down menus. Is that correct? And if so, how would I be able to show the project name easily for each object that the query is returning?
Unfortunately Project is not a hydratable field. In general the fields that can be hydrated are dropdown fields as you mentioned in your question.
The best way to do what you need is to use a Rally.data.WsapiDataStore to query for the projects in your current workspace and to build an in-memory map of OID to name.
var projects = {};
Ext.create('Rally.data.WsapiDataStore', {
model: 'Project',
autoLoad: true,
limit: Infinity,
fetch: ['Name', 'ObjectID'],
context: this.getContext().getDataContext(),
listeners: {
load: function(store, records) {
Ext.Array.each(records, function(record) {
projects[record.get('ObjectID')] = record.get('Name');
});
}
}
});

Categories