Live link: ProductHuntClone
Producthuntclone is my best attempt at cloning producthunt's overall design and functionalites, it's a web application that allow uers to post products that they wanna pitch, made or hunt for. As an annonymous user you can only view all products posted, disucssions and comments. As a registered user you can posts new products, create discussions, create comments, upvote products or discussions, follow users, and search for users, products and discussions.
- Clone this repository
- Install dependencies
npm install - Create a
.envfile based on the.env.example - Set up your PostgreSQL user and password.
- Make sure to create the db
npx dotenv sequelize-cli db:create - Migrate the models
npx dotenv sequelize-cli db:migrate - Populate the data with seeders found in "backend/db/seeders"
npx dotenv sequelize-cli db:seed:all - Now run the application
npm start
- Non users can view profiles, products, discussions.
- Users can view, edit, create, delete products.
- Users can edit, view profile and other users profile.
- Users can post and view discussions.
- Users can upvote products.
--not yet implemented--
- comments,
- searchbar(current implementation is anyone can look up a user and visit their profile through url
https://producthuntclone.herokuapp.com/@[USER_NAME]) or click a users icon - edit and delete discussions
Why lodash? It provides a fully implemented throttler function, what is a throttler function? Basically it throttles multiple function calls and limits it to the most latest call, reason for it being needed in infinite scroll is when you scroll an event fires and for this particular use case as soon as you reach bottom of the page without a throttler multiple scroll events fire even when you have logic that supports current scroll coordinate == bottom document height.
const products = useSelector((state)=>{
return state.products.list.map((productId) => state.products[productId])
})
let pageCounter = 1
const throttler = _.throttle(scroll, 500)
let sortedProducts = {}
Object.keys(products).map((key) =>{
let str = products[key].createdAt
str = str.substring(0, str.length - 4);
if(!sortedProducts[str]){
sortedProducts[str] = []
return sortedProducts[str].push(products[key])
} else {
return sortedProducts[str].push(products[key])
}
})
useEffect(()=>{
if(window.addEventListener){
window.addEventListener('scroll', throttler, true);
window.addEventListener('scroll', scrollToTopChecker);
}
return function cleanup(){
window.removeEventListener('scroll', throttler, true);
window.removeEventListener('scroll', scrollToTopChecker);
}
}, [throttler])
const setNextPage = async () => {
await dispatch(getProducts(pageCounter + 1))
pageCounter += 1
}
function scroll(){
const pixelFromTopToBottom = Math.max(document.documentElement.scrollTop,document.body.scrollTop);
if((pixelFromTopToBottom+document.documentElement.clientHeight) >= document.documentElement.scrollHeight ){
setNextPage()
}
}This block of code checks the current users upvotes and applies a class voted__true to the corresponding element.
If there is no current user logged on then no change happens on the element and the first line of code makes it so that non logged in users,
can't upvote
const [disableVote, setDisableVote] = useState(false)
let upvoted = useSelector((state)=>{
if(state.session.user){
user = state.session.user
for (const [key, value] of Object.entries(state.session.upvotes)){
if(key){
if(products.id === value.id){
return true
}
}
}
}
})
useEffect(() => {
if(upvoted){
upvoteElementRef.current.classList.add('voted__true')
}
},[user, upvoted])This block of code makes it so users can't spam the upvote button in quick succession, only allowing them to click the button after the dispatch finishes, it also makes an animation when an upvote occurs through the use of that setTimeout.
const vote = async () =>{
if(user){
if (upvoteElementRef.current.classList.contains('voted__true')){
setDisableVote(true)
await upvoteElementRef.current.classList.remove('voted__true')
await dispatch(voteProduct(products.id))
setUpvotes(getUpvotes - 1)
setDisableVote(false)
} else {
setDisableVote(true)
await triangleRef.current.classList.add('hidden')
await circleRef.current.classList.remove('hidden')
await circleRef.current.classList.add('scale')
setTimeout(()=>{
triangleRef.current.classList.remove('hidden')
circleRef.current.classList.remove('scale')
circleRef.current.classList.add('hidden')
}, 200)
await upvoteElementRef.current.classList.add('voted__true')
await dispatch(voteProduct(products.id))
setUpvotes(getUpvotes + 1)
setDisableVote(false)
}
}
}