|
// Velo API Reference: https://www.wix.com/velo/reference/api-overview/introduction |
|
/** |
|
* TikCommerce prototype code |
|
Copyright (C) 2024 Paul |
|
|
|
This program is free software: you can redistribute it and/or modify |
|
it under the terms of the GNU General Public License as published by |
|
the Free Software Foundation, either version 3 of the License, or |
|
(at your option) any later version. |
|
|
|
This program is distributed in the hope that it will be useful, |
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
GNU General Public License for more details. |
|
|
|
You should have received a copy of the GNU General Public License |
|
along with this program. If not, see <https://www.gnu.org/licenses/>. |
|
*/ |
|
|
|
import wixWindow from 'wix-window' |
|
import wixData from 'wix-data' |
|
import wixWindowFrontend from "wix-window-frontend" |
|
import wixLocationFrontend from "wix-location-frontend" |
|
/* import IntersectionObserver from "intersection-observer" |
|
import TouchSweep from 'touchsweep' */ |
|
|
|
// import { cart } from "wix-stores" |
|
import { local } from "wix-storage-frontend" |
|
|
|
|
|
/* |
|
const popUpEvent = new Event("popUpOpen") |
|
|
|
|
|
function respondToVisibility(element, callback) { |
|
var options = { |
|
root: $w("#page1"), |
|
}; |
|
|
|
var observer = new IntersectionObserver((entries, observer) => { |
|
entries.forEach(entry => { |
|
callback(entry.intersectionRatio > 0); |
|
}); |
|
}, options); |
|
|
|
observer.observe(element); |
|
} |
|
|
|
*/ |
|
|
|
let productData = { |
|
current: {}, // stores current data |
|
prev: {}, // stores prev data |
|
addToCart: false, // indicates if the current product is added to cart, if has been added to cart, keep recommending similar items |
|
} |
|
|
|
let customCart = [] |
|
|
|
let productsCache = [] // ideally you will preload maxium 10 products, but for demo I am loading all |
|
|
|
$w.onReady(function () { |
|
|
|
|
|
for (let x=1; x<=7; x++){ |
|
|
|
const shortsContainer = $w(`#shortsContainer${x}`) |
|
//console.log("element: ", shortsContainer, x) |
|
|
|
for (let child of shortsContainer.children){ |
|
// find the product name |
|
if (child.customClassList.contains("product-name-container")){ |
|
const productName = child.children[0].text // get the product name, (text inside the container) |
|
shortsContainer.onClick(() => openVideoPopUp(productName)) // when clicked open video popup |
|
|
|
break |
|
} |
|
} |
|
|
|
shortsContainer.onMouseIn(showProductNameOnhover) |
|
shortsContainer.onMouseOut(hideProductNameOnLeave) |
|
} |
|
|
|
$w("#video-popup").customClassList.add("hidden") |
|
|
|
let cart = local.getItem("cart") |
|
|
|
|
|
if (!cart){ |
|
local.setItem("cart", JSON.stringify([])) |
|
}else{ |
|
|
|
const cartCount = $w("#cart-item-count") |
|
cartCount.text = `${JSON.parse(cart).length}` |
|
} |
|
|
|
|
|
wixData.query('Products') |
|
.find() |
|
.then((results) => { |
|
if (results.items.length > 0) { |
|
productsCache = results.items.filter(item => item.productName) // remove if the product name is undefined |
|
|
|
} else { |
|
console.log('No items found'); |
|
} |
|
}) |
|
.catch((err) => { |
|
console.error('Query failed:', err) |
|
}) |
|
|
|
$w("#cart-container").onClick(() => { |
|
wixLocationFrontend.to("/cart") |
|
}) |
|
|
|
$w("#nextButton").onClick(nextProduct) |
|
|
|
//$w("#video-container").addEventListner() |
|
|
|
if (wixWindowFrontend.formFactor == "Mobile"){ |
|
|
|
$w("#review-open-btn").onClick(openReview) |
|
$w("#close-review-btn").onClick(closeReview) |
|
$w("#next-prod-btn").onClick(nextProduct) |
|
}else{ |
|
const reviewBox = $w("#reviewBox") |
|
reviewBox.show() |
|
} |
|
|
|
$w("#searchContainer").hide() // by default the search container must be hidden |
|
|
|
$w("#closeSearch").onClick(() => { |
|
$w("#searchContainer").hide("zoom", {duration: 200}) |
|
$w("#searchBox").value = "" // upon closing the search results, clear the search |
|
}) |
|
|
|
$w("#searchBox").onKeyPress((event) => { |
|
if (event.key === "Enter") { |
|
openSearchResults() |
|
} |
|
}) |
|
|
|
|
|
}) |
|
|
|
|
|
/** |
|
* In mobile devices, the reviews are hidden by default, so to open the user has to press on the comment button |
|
*/ |
|
export function openReview(){ |
|
|
|
const reviewBox = $w("#reviewBox") |
|
|
|
reviewBox.show("slide", {direction: "bottom"}) |
|
|
|
} |
|
|
|
/** |
|
* In mobile devices, the reviews are hidden by default, so to close the user has to press on the close button |
|
*/ |
|
export function closeReview(){ |
|
|
|
const reviewBox = $w("#reviewBox") |
|
|
|
reviewBox.hide("slide", {direction: "bottom"}) |
|
|
|
} |
|
|
|
|
|
|
|
let audienceScoreInterval = null |
|
|
|
/** |
|
[Read more](https://www.wix.com/corvid/reference/$w.ViewportMixin.html#onViewportEnter) |
|
* @param {string} score |
|
*/ |
|
export function audienceTextEnterAnimation(score) { |
|
// this function will animate the audience score text |
|
|
|
const audienceScore = $w("#audience-score") |
|
audienceScore.show() |
|
|
|
|
|
let finalScore = parseInt(score || audienceScore.text) |
|
let count = 0 |
|
audienceScore.text = "0" |
|
|
|
clearInterval(audienceScoreInterval) |
|
|
|
audienceScoreInterval = setInterval(() => { |
|
count ++ |
|
audienceScore.text = `${count}` |
|
|
|
if (count === finalScore || count === 100){ |
|
//prevent infinite loop, so set if count === 100 |
|
clearInterval(audienceScoreInterval) |
|
} |
|
|
|
}, 25); |
|
|
|
} |
|
|
|
let progressBarInterval = null |
|
|
|
export function progressBarAnimation(rating) { |
|
// this function will animate the audience score text |
|
|
|
const ratingSlider = $w("#ratingSlider") |
|
ratingSlider.show() |
|
|
|
|
|
let finalScore = parseInt(rating) |
|
let count = 0 |
|
ratingSlider.value = count |
|
|
|
clearInterval(progressBarInterval) |
|
|
|
progressBarInterval = setInterval(() => { |
|
count += 1 |
|
ratingSlider.value = count |
|
|
|
if (count === finalScore || count === 100){ |
|
//prevent infinite loop, so set if count === 100 |
|
clearInterval(progressBarInterval) |
|
} |
|
|
|
}, 30); |
|
|
|
} |
|
|
|
|
|
/** |
|
* Adds an event handler that runs when the element is clicked. |
|
[Read more](https://www.wix.com/corvid/reference/$w.ClickableMixin.html#onClick) |
|
* @param {$w.MouseEvent} event |
|
*/ |
|
export function closeVideoPopUp(event) { |
|
// function to close the video popup |
|
const popUp = $w('#video-popup') |
|
|
|
popUp.hide("zoom", {duration: 500}) |
|
setTimeout(() => popUp.customClassList.add("hidden"), 500) // after 500 ms hide so it doesn't affect the animation |
|
|
|
if (wixWindowFrontend.formFactor == "Mobile"){ |
|
// close the reviews when the popup video is hidden |
|
$w("#reviewBox").hide() |
|
} |
|
|
|
} |
|
|
|
/** |
|
* Given a product name returns the product from cached product list |
|
*/ |
|
function getProductFromName(name){ |
|
|
|
return productsCache.find(item => { |
|
return item.productName === name |
|
}) |
|
} |
|
|
|
/** |
|
* Adds an event handler that runs when the element is clicked. |
|
[Read more](https://www.wix.com/corvid/reference/$w.ClickableMixin.html#onClick) |
|
* @param {string} productName |
|
*/ |
|
export function openVideoPopUp(productName) { |
|
// opens video popup |
|
/* const elementId = event.target.id |
|
|
|
const element = $w(`#${elementId}`) |
|
|
|
if (wixWindowFrontend.formFactor === "Mobile"){ |
|
$w("#reviewBox").hide() |
|
} |
|
|
|
let productName = "" |
|
|
|
element.children.forEach((child) => { |
|
const childElement = $w(`#${child.id}`) // ensure reliability, as just accessing child directly would provide with the updated elements |
|
console.log("class list: ", childElement.children, childElement.customClassList.contains("product-name-container")) |
|
if (childElement.customClassList.contains("product-name-container")){ |
|
productName = childElement.children[0].text // get the product name, (text inside the container) |
|
|
|
console.log("cjoldren: ", childElement.children[0].text) |
|
} |
|
|
|
}) |
|
//console.log("prduct name: ", productName) |
|
|
|
console.log("productName: ", productName, element.children) |
|
|
|
if (productName === ""){ |
|
return |
|
} |
|
*/ |
|
const product = getProductFromName(productName) |
|
|
|
if (product){ |
|
const popUp = $w('#video-popup') |
|
popUp.customClassList.remove("hidden") |
|
|
|
popUp.show("zoom", {duration: 500}) |
|
|
|
productData.current = product |
|
// popUp = results[0] |
|
|
|
updateProductOnPopUp(productData.current) |
|
}else{ |
|
console.log("cannot find the product") |
|
} |
|
|
|
// querying is slower, so cache few results, check if it exists in cache, if not the call the query |
|
/*wixData.query('Products') |
|
.eq('productName', productName) |
|
.find() |
|
.then(results => { |
|
//console.log('Results:', results); |
|
|
|
if (results.length === 0){ |
|
return |
|
} |
|
|
|
const popUp = $w('#video-popup') |
|
popUp.customClassList.remove("hidden") |
|
|
|
popUp.show("zoom", {duration: 500}) |
|
|
|
productData.current = results.items[0] |
|
// popUp = results[0] |
|
|
|
updateProductOnPopUp(productData.current) |
|
|
|
}) |
|
.catch(err => { |
|
console.error('Error:', err); |
|
|
|
});*/ |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export function updateProductOnPopUp(item){ |
|
// given an items updates the view of the product on the popup |
|
|
|
const audienceScore = $w("#audience-score") |
|
audienceScore.text = item.rating |
|
|
|
if (wixWindowFrontend.formFactor === "Mobile"){ |
|
$w("#reviewBox").hide() |
|
} |
|
|
|
setTimeout(() => { |
|
progressBarAnimation(item.rating) |
|
}, 1) |
|
|
|
|
|
setTimeout(() => { |
|
audienceTextEnterAnimation(item.rating) // animate user rating |
|
}, 5) |
|
|
|
//$w('#ratingSlider').value = item.rating |
|
|
|
const popUpVideo = $w("#previewVideo") |
|
popUpVideo.src = item.productVideo |
|
|
|
const productName = $w("#product-name") |
|
productName.text = item.productName |
|
|
|
const buyNowBtn = $w("#buy-now-btn") |
|
buyNowBtn.label = `Buy Now - $${item.price}` |
|
|
|
const ratingEmoji = $w("#ratingEmoji") |
|
|
|
|
|
if (item.rating > 70){ |
|
ratingEmoji.text = "😍" |
|
}else if (item.rating > 50 && item.rating <= 70){ |
|
ratingEmoji.text = "😚" |
|
}else{ |
|
ratingEmoji.text = "🤡" |
|
} |
|
|
|
ratingEmoji.show("zoom", {duration: 100}) |
|
|
|
//console.log("rating emoju: ", item.productName) |
|
|
|
wixData.query('ProductReviews') |
|
.eq('productName', item._id) |
|
.include("user") |
|
.find() |
|
.then(results => { |
|
// console.log("reviews: ", results) |
|
updateReviews(results.items) |
|
}).catch((err) => { |
|
console.log("an error occurred retreiving revirews: ", err) |
|
}) |
|
|
|
|
|
} |
|
|
|
/** |
|
* |
|
* @param {any[]} reviews |
|
*/ |
|
export function updateReviews(reviews){ |
|
|
|
const repeater = $w('#reviewRepeater') |
|
|
|
const reviewMessage = $w("#review-message-text") |
|
if (reviews.length === 0){ |
|
|
|
reviewMessage.text = "Be the first to review this product 😍" |
|
reviewMessage.show() |
|
repeater.hide() |
|
return |
|
|
|
}else{ |
|
reviewMessage.hide() |
|
repeater.show() |
|
} |
|
|
|
repeater.onItemReady(($item, itemData) => { |
|
|
|
$item('#review-description').text = itemData.reviewText |
|
$item('#reviewer-name').text = itemData.user.username |
|
$item("#profile-pic").src = itemData.user.profilePicture |
|
|
|
$item('#review-emoji').text = itemData.rating >= 3 ? "😍" : "🤡" |
|
}) |
|
|
|
|
|
repeater.data = reviews |
|
} |
|
|
|
/** |
|
* Adds an event handler that runs when the pointer is moved |
|
onto the element. |
|
|
|
You can also [define an event handler using the Properties and Events panel](https://support.wix.com/en/article/velo-reacting-to-user-actions-using-events). |
|
[Read more](https://www.wix.com/corvid/reference/$w.Element.html#onMouseIn) |
|
* @param {$w.MouseEvent} event |
|
*/ |
|
export function showProductNameOnhover(event) { |
|
// This function was added from the Properties & Events panel. To learn more, visit http://wix.to/UcBnC-4 |
|
|
|
const elementId = event.target.id |
|
|
|
const element = $w(`#${elementId}`) |
|
|
|
const children = element.children |
|
|
|
children.forEach((ele) => { |
|
|
|
if (ele.customClassList.values().includes("product-name-container")){ |
|
ele.show("roll", {duration: 200, direction: "bottom"}) |
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
export function hideProductNameOnLeave(event) { |
|
// This function was added from the Properties & Events panel. To learn more, visit http://wix.to/UcBnC-4 |
|
|
|
const elementId = event.target.id |
|
|
|
const element = $w(`#${elementId}`) |
|
|
|
const children = element.children |
|
|
|
children.forEach((ele) => { |
|
|
|
if (ele.customClassList.values().includes("product-name-container")){ |
|
ele.hide("roll", {duration: 200, direction: "bottom"}) |
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
function findProductIndex(productId, productList) { |
|
// Helps check for duplicate item in the cart, method to find the index of the object with the given productId if doesn't exist returns -1 |
|
return productList.findIndex(product => product.productId === productId); |
|
} |
|
|
|
/** |
|
* Adds an event handler that runs when the element is double-clicked. |
|
[Read more](https://www.wix.com/corvid/reference/$w.ClickableMixin.html#onDblClick) |
|
* @param {$w.MouseEvent} event |
|
*/ |
|
export function addToCart(event) { |
|
// This function was added from the Properties & Events panel. To learn more, visit http://wix.to/UcBnC-4 |
|
// Add your code for this event here: |
|
|
|
const addToCart = $w("#addToCartAnimation") |
|
const cartCount = $w("#cart-item-count") |
|
|
|
|
|
addToCart.hide("fade") |
|
addToCart.show("zoom", {duration: 200}) |
|
|
|
setTimeout(() => { |
|
addToCart.hide("fade", {duration: 100}) |
|
}, 300) |
|
|
|
if (!productData.current) |
|
return |
|
|
|
|
|
const existingCart = JSON.parse(local.getItem("cart")) |
|
|
|
const productIndex = findProductIndex(productData.current.productName, existingCart) |
|
|
|
productData.addToCart = true // current item has been added to cart, recommend similar items |
|
|
|
if (productIndex > -1){ |
|
existingCart[productIndex].quantity += 1 |
|
local.setItem("cart", JSON.stringify(existingCart)) |
|
}else{ |
|
|
|
customCart = [{productId: productData.current.productName, quantity: 1}, ...existingCart] |
|
|
|
|
|
cartCount.text = `${customCart.length}` |
|
|
|
local.setItem("cart", JSON.stringify(customCart)) |
|
} |
|
|
|
/*cart.addProducts([{ |
|
productId: productData.current.productName, |
|
quantity: 1, |
|
} |
|
]).then(() => { |
|
console.log("added to cart2") |
|
}).catch((err) => { |
|
console.log("error occurred: ", err) |
|
}) |
|
|
|
console.log("added to cart3") |
|
*/ |
|
|
|
} |
|
|
|
function filterItemsByTags(items, tags) { |
|
return items.filter(item => { |
|
return item.tags.some(tag => tags.includes(tag)); |
|
}) |
|
} |
|
|
|
/** |
|
* |
|
* @param {object[]} items |
|
* @param {string[]} queryTags |
|
* Given items and query tags, returns the item that has the most matches with the querytags |
|
* Note: make sure the product is not undefined |
|
*/ |
|
function getItemWithMostMatches(items, queryTags){ |
|
|
|
|
|
const itemsWithMatchCount = items.map(item => { |
|
const matchCount = item.productTags?.filter(tag => queryTags.includes(tag)).length |
|
return { ...item, matchCount } |
|
}) |
|
|
|
const itemWithMostMatches = itemsWithMatchCount.reduce((maxItem, currentItem) => { |
|
return (currentItem.matchCount || 0 > maxItem.matchCount) ? currentItem : maxItem |
|
}) |
|
|
|
return itemWithMostMatches |
|
} |
|
|
|
|
|
function getRandomItemExcluding(excludeItemName) { |
|
const filteredItems = productsCache.filter(item => item.productName !== excludeItemName) |
|
const randomIndex = Math.floor(Math.random() * filteredItems.length) |
|
|
|
return filteredItems[randomIndex] |
|
} |
|
|
|
|
|
export function nextProduct(){ |
|
|
|
// const nextprod = filterItemsByTags(productsCache, productData.current.productTags) |
|
|
|
let nextprod = getItemWithMostMatches(productsCache, productData.current.productTags) |
|
|
|
if (!productData.addToCart){ |
|
nextprod = getRandomItemExcluding(productData.current?.productName) |
|
} |
|
|
|
//console.log("next product: ", nextprod) |
|
|
|
productData = { |
|
current: nextprod, // stores current data |
|
prev: productData.current, // stores prev data |
|
addToCart: false, // reset the addToCart |
|
} |
|
|
|
updateProductOnPopUp(productData.current) |
|
|
|
} |
|
|
|
function searchProductsByName(products, startChars) { |
|
return products.filter(product => product.productName.toLowerCase().startsWith(startChars.toLowerCase())) |
|
} |
|
|
|
/** |
|
* Search functionality |
|
*/ |
|
|
|
function openSearchResults(){ |
|
|
|
$w("#searchNotFoundText").hide() |
|
$w("#searchContainer").show() |
|
|
|
|
|
let productName = $w("#searchBox").value |
|
|
|
$w("#searchText").text = `"${productName}"` |
|
|
|
const results = searchProductsByName(productsCache, productName) |
|
|
|
const searchRepeater = $w("#searchRepeater") |
|
if (results.length === 0){ |
|
$w("#searchNotFoundText").show() |
|
searchRepeater.hide() |
|
|
|
return |
|
} |
|
|
|
// console.log("search rsults: ", results) |
|
|
|
searchRepeater.data = results |
|
searchRepeater.show() |
|
|
|
searchRepeater.onItemReady(($item, itemData) => { |
|
|
|
$item('#searchResultName').text = itemData.productName |
|
$item('#searchProductImage').src = itemData.image |
|
|
|
$item("#shortsSearchContainer").onClick(() => openVideoPopUp(itemData.productName)) |
|
|
|
}) |
|
|
|
} |