-
Notifications
You must be signed in to change notification settings - Fork 11
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
Fullstack challenge - Jed Stanton #4
base: main
Are you sure you want to change the base?
Conversation
Initially users weren't able to view any products unless shop_id param was present. In this commit I've also optimised some of the existing queries as well as removed string injection in sql queries.
In order to allow a user to create a new review, I made the following changes: - Add new route to create a review - Add create review work so we can do this asynchronously - Added a new form to create a review - As part of the new review form, I added a new field to allow the user to select a product however in order to improve performance the dropdown only populates when a user types text into search field
Are you actually able to run the app without upgrading any dependencies (Ruby, gems, JS packages)? Or did you upgrade some of them but didn't commit? |
Thanks for the demo videos, they are very helpful! |
.field | ||
= form.label :shop_id, 'Shop' | ||
= form.collection_select :shop_id, @shops, :id, :name, prompt: 'Select a Shop' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shop_id
field is redundant.
const searchInput = document.createElement('input'); | ||
searchInput.type = 'text'; | ||
searchInput.placeholder = 'Search for a product'; | ||
searchInput.id = 'product-search'; | ||
productSelect.parentNode.insertBefore(searchInput, productSelect); | ||
|
||
let timeout = null; | ||
|
||
searchInput.addEventListener('input', function () { | ||
clearTimeout(timeout); | ||
timeout = setTimeout(function () { | ||
const query = searchInput.value.trim(); | ||
if (query.length > 4) { | ||
fetch(`/products/search?q=${encodeURIComponent(query)}`) | ||
.then(response => response.json()) | ||
.then(data => { | ||
productSelect.innerHTML = '<option value="">Select a Product</option>'; | ||
data.items.forEach(product => { | ||
const option = document.createElement('option'); | ||
option.value = product.id; | ||
option.textContent = product.title; | ||
productSelect.appendChild(option); | ||
}); | ||
}) | ||
.catch(error => console.error('Error fetching products:', error)); | ||
} else { | ||
productSelect.innerHTML = '<option value="">Select a Product</option>'; | ||
} | ||
}, 300); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm impressed!
products.each do |product| | ||
product_reviews_page_data = params[:product_reviews_page_data] || {} | ||
page = product_reviews_page_data[product.id.to_s]&.to_i || 1 | ||
reviews = product.reviews.order(created_at: :desc).limit(REVIEWS_PER_PAGE).offset((page.to_i - 1) * REVIEWS_PER_PAGE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you sort product reviews by created_at DESC but sort products by created ASC?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it made sense to show the most recently created review first but with the products I thought most recently added gets tagged on to the last page made sense. However thinking about it now they should probably both be DESC
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem here. I'm just curious about your thoughts.
products_query = products_query.where(shop_id: params[:shop_id]) if params[:shop_id].present? | ||
products_query = products_query.order(created_at: :asc) | ||
|
||
products = products_query.includes(:reviews).limit(params[:per_page]).offset(offset) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need .includes(:reviews)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would there not be a N+1 without it on line 22?
reviews = product.reviews.order(created_at: :desc).limit(REVIEWS_PER_PAGE).offset((page.to_i - 1) * REVIEWS_PER_PAGE)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the difference in generated SQL queries between with and without includes(:reviews)
?
@@ -41,4 +65,7 @@ def tags_with_default(params) | |||
default_tags.concat(params[:tags].split(',')).uniq | |||
end | |||
|
|||
def review_params | |||
params.require(:review).permit(:rating, :body, :reviewer_name, :product_id, :shop_id, :tags) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
review_params[:tags]
is not used anywhere
products.each do |product| | ||
product_reviews_page_data = params[:product_reviews_page_data] || {} | ||
page = product_reviews_page_data[product.id.to_s]&.to_i || 1 | ||
reviews = product.reviews.order(created_at: :desc).limit(REVIEWS_PER_PAGE).offset((page.to_i - 1) * REVIEWS_PER_PAGE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: page.to_i
is redundant
did end up having to upgrade a few gems and js packages but didn't end up committing them |
}) | ||
.then(response => response.text()) | ||
.then(data => { | ||
eval(data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This AJAX way is not recommended but it works, so still fine to me.
Thanks as I'm not distracted by a big PR. |
My Process
Started with challenge two:
I initially cleaned up the index action, removing any n+1s, string injection and using activerecord methods for sort/offset/limit etc rather than ruby.
Having set that up I moved on to creating the index view. Being mindful of time I just did all the styling inline but if I had more time I would've moved them into a scss file.
I then moved on to pagination for reviews for each product. I then created two new partials one which will render the reviews for each product and another which some very basic pagination (list all the pages of the reviews which is calculated by the total number of reviews for each product which i pass in the data instance variable). I added some javascript to send an AJAX request to a new route + action I created, it'll pass the page the user has clicked and product id. The action will then rerender the reviews partial for that particular product. Improvements I would make to this if I had more time would be to improve the UI/UX of pagination partial, currently just a list of all the page numbers rather than what was specific on the design.
I did not have time to add pagination to the product pages, however if I did I probably would've used the kaminari gem.
I did not have time to add tests here but if I did I would test the following.
Screen.Recording.2024-05-20.at.15.37.53.mov
Challenge 1:
Added new route for reviews and then a very basic form to create a new review as I did not have a enough time to style it to match design. As it was a standalone page rather than a button on each product on the index page for the product dropdown rather than listing all product which would have made the dropdown v slow I added a search field for the user to type in to populate the dropdown, once the user had typed at least 5 letters this sent an ajax request to a new route that I had added which then sent the results of the query back to the javascript where I populated the dropdown. I also changed the create action to async call the worker i created to make the reviews plus added validations to the review model to ensure valid reviews were being created.
Again here I unfortunately did not the time to added tests, however had I done:
Screen.Recording.2024-05-20.at.15.38.40.mov
I also would've tidied up my commits to make them as atomic as possible if I had more time