// Import our custom CSS
// import { get } from 'cypress/types/lodash';
import "../../scss/styles.scss";
import { API_BASE, API_LISTINGS, API_GET_LISTINGS_PARAMS } from "../settings.mjs";
import { ErrorHandler } from "../shared/errorHandler.mjs";
import { displaySpinner } from "../shared/displaySpinner.mjs";
import { displayError } from "../shared/displayErrorMsg.mjs";
import { sanitize } from "../shared/sanitize.mjs";
import { handleBidSubmit } from "./bidOnListing.mjs";
import { load } from "../shared/storage.mjs";
/** @typedef GetAuctionListingsDataResponse
* @type {object}
* @property {string} id
* @property {string} title
* @property {string} description
* @property {object[]} media
* @property {string} media.url
* @property {string} media.alt
* @property {string[]} tags
* @property {string} created
* @property {string} updated
* @property {string} endsAt
* @property {object[]} bids
* @property {string} bids.id
* @property {number} bids.amount
* @property {object} bids.bidder
* @property {string} bids.bidder.name
* @property {string} bids.bidder.email
* @property {string} bids.bidder.bio
* @property {object} bids.bidder.avatar
* @property {string} bids.bidder.avatar.url
* @property {string} bids.bidder.avatar.alt
* @property {object} bids.bidder.banner
* @property {string} bids.bidder.banner.url
* @property {string} bids.bidder.banner.alt
* @property {string} bids.created
* @property {object} seller
* @property {string} seller.name
* @property {string} seller.email
* @property {null} seller.bio
* @property {object} seller.avatar
* @property {string} seller.avatar.url
* @property {string} seller.avatar.alt
* @property {object} seller.banner
* @property {string} seller.banner.url
* @property {string} seller.banner.alt
* @property {object} _count
* @property {number} _count.bids
*/
/** @typedef {object} GetAuctionListingsResponse
* @property {GetAuctionListingsDataResponse[]} data
* @property {GetAuctionListingsDataResponse} item
*/
export function init() {
/** @type {HTMLInputElement} */
const txtFilter = document.querySelector("#filter"); // input
txtFilter.addEventListener("input", handleSearchInput);
displayListings();
}
/** @type {Array<GetAuctionListingsDataResponse>} */
let data = [];
/**
* @description Send a request to API
* @async
* @function displayListings
* @returns {Promise<GetAuctionListingsDataResponse[]|null|undefined>}If response is ok, return listings info. If response is not ok, return null. Returns undefined for unexpected errors.
* */
export async function displayListings() {
try {
displaySpinner(true, "#spinnerListings");
displayError(false, "#errorListings");
//This endpoint does not require authentication.
const url = API_BASE + API_LISTINGS + API_GET_LISTINGS_PARAMS;
const response = await fetch(url, {
headers: {
// Authorization: `Bearer ${load("token")}`,
// "X-Noroff-API-Key": API_KEY,
"Content-Type": "application/json",
},
method: "GET",
});
if (response.ok) {
/** @type {GetAuctionListingsResponse} */
const listingsData = await response.json();
data = listingsData.data;
updateListings(data, "");
return data;
}
const eh = new ErrorHandler(response);
const msg = await eh.getErrorMessage();
displayError(true, "#errorListings", msg);
return null;
} catch (ev) {
displayError(true, "#errorListings", "Could not show the listings!");
} finally {
displaySpinner(false, "#spinnerListings");
}
}
/**
* @description Display listings, filtered by searchInput.
* @method updateListings
* @param {Array<GetAuctionListingsDataResponse|undefined>} data Listings to be shown.
* @param {string} searchInput The text to be found. If empty returns all listings.
*/
export async function updateListings(data, searchInput) {
/** @type {HTMLDivElement} */
const listings = document.querySelector("#listings");
listings.innerHTML = "";
if (data.length === 0) {
listings.innerHTML = "No listings found!";
return;
}
data
.filter((listing) => {
return listing.media.length > 0;
})
.filter((listing) => {
const searchText = searchInput.toLowerCase();
const title = (listing.title || "").toLowerCase();
const body = (listing.description || "").toLowerCase();
if (title.includes(searchText) || body.includes(searchText)) {
return true;
}
return false;
})
.map((x) => generateHtml(x))
.forEach((x) => {
listings.appendChild(x);
});
return;
}
/**
* @description Map a listing to html content
* @function generateHtml
* @param {GetAuctionListingsDataResponse} item The listing properties
* @returns {Object} Return the object listing
*/
function generateHtml(item) {
const { title, endsAt, seller, media, description, created } = item;
//An unregistered user may view through Listings
const isAuth = !!load("token");
/** @type {HTMLTemplateElement} */
const template = document.querySelector("#listing");
const listing = /** @type {HTMLDivElement} */ (template.content.cloneNode(true));
listing.querySelector("article").dataset.id = item.id;
/** @type Intl.DateTimeFormatOptions */
const options = {
// weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
};
// listing.querySelector("#deadline").innerHTML = endsAt;
const endsAtDate = new Date(endsAt);
if (endsAtDate > new Date()) {
let endsAtDateString = endsAtDate.toLocaleDateString("no-NO", options);
listing.querySelector("#deadline").innerHTML = `Expires ${endsAtDateString}`;
} else {
listing.querySelector("#deadline").innerHTML = "EXPIRED";
}
listing.querySelector("h5").innerText = seller.name; // + item.id
/** @type {HTMLImageElement} */
const sellerImg = listing.querySelector("#sellerImg");
sellerImg.src = seller.avatar.url;
let date = new Date(created);
// `BCP 47 language tag` => no-NO
let dateString = date.toLocaleDateString("no-NO", options);
listing.querySelector("#dateListing").innerHTML = dateString;
listing.querySelector("#bodyTitle").innerHTML = sanitize(title);
const bodyText = listing.querySelector("#viewListing");
let descriptionSanitized = sanitize(description);
bodyText.innerHTML = descriptionSanitized;
const history = listing.querySelector("#history");
const hasBids = item.bids.length > 0;
if (!hasBids || !isAuth) {
history.classList.add("d-none");
}
//The entry point code for bid on listing
if (isAuth) {
if (hasBids) {
const bids = item.bids.sort((v1, v2) => new Date(v2.created).valueOf() - new Date(v1.created).valueOf());
const maxBid = bids[0].amount;
/** @type {HTMLInputElement} */
const amountBid = listing.querySelector("#amountBid");
amountBid.min = String(maxBid + 0.01);
const bidsHtml = bids.map((bid) => {
const date = new Date(bid.created).toLocaleDateString("no-NO", options);
return `<dt class="col-3"><span><i class="bi bi-gem"></i></span> ${bid.amount}</dt><dd class="col-9">${date} by ${bid.bidder.name}</dd>`;
});
history.querySelector("dl").innerHTML = bidsHtml.join("");
}
listing.querySelector(`#btnPlaceBid`).classList.remove("d-none");
listing.querySelector(`#btnPlaceBid`).addEventListener("click", (ev) => {
ev.preventDefault();
const placeBid = document.querySelector(`article[data-id="${item.id}"] #btnPlaceBid`);
placeBid.classList.add("d-none");
const PlaceOnListing = document.querySelector(`article[data-id="${item.id}"] #PlaceOnListing`);
PlaceOnListing.classList.remove("d-none");
PlaceOnListing.classList.add("d-flex");
});
listing.querySelector("#createBid").addEventListener("submit", async (ev) => {
ev.preventDefault();
handleBidSubmit(item.id, ev);
});
} else {
listing.querySelector(`#linkPlaceBid`).classList.remove("d-none");
}
if (media) {
const listingImgs = listing.querySelector("#listingImgs");
/** @type {HTMLTemplateElement} */
const templateCarousel = document.querySelector("#slider-carousel");
const CloneCarousel = /** @type {HTMLDivElement} */ (templateCarousel.content.cloneNode(true));
const carouselNewId = `carousel_${item.id}`;
CloneCarousel.querySelector(".carousel").id = carouselNewId;
/** @type {HTMLButtonElement} */
const prev = CloneCarousel.querySelector(".carousel-control-prev");
prev.dataset.bsTarget = `#${carouselNewId}`;
/** @type {HTMLButtonElement} */
const next = CloneCarousel.querySelector(".carousel-control-next");
next.dataset.bsTarget = `#${carouselNewId}`;
const imgs = CloneCarousel.querySelector(".carousel-inner");
imgs.innerHTML = "";
for (let i = 0; i < media.length; i++) {
const img = media[i];
imgs.innerHTML += `<div class="carousel-item ${i === 0 ? "active" : ""}"><img src="${img.url}" class="d-block w-100 card-img" alt="${img.alt}"></div>`;
}
if (media.length <= 1) {
prev.style.display = "none";
next.style.display = "none";
}
listingImgs.appendChild(CloneCarousel);
}
return listing;
}
/**
* @description Handle the search submit.
* @method handleSearchInput
* @param {*} ev
*/
async function handleSearchInput(ev) {
const userInput = /** @type {HTMLInputElement} */ ev.currentTarget.value;
updateListings(data, userInput);
}