diff --git a/README.md b/README.md index 9c47377b..a7f97b1e 100644 --- a/README.md +++ b/README.md @@ -81,9 +81,12 @@ service cloud.firestore { function isLoggedIn() { return exists(/databases/$(database)/documents/users/$(request.auth.uid)) } - match /users/{user} { - allow read, update, delete: if false; - allow create: if true; + // Make sure the uid of the requesting user matches name of the user + // document. The wildcard expression {userId} makes the userId variable + // available in rules. + match /users/{userId} { + allow read, update, delete: if request.auth != null && request.auth.uid == userId; + allow create: if request.auth != null; } match /auction-live/{items} { allow get, list: if true; diff --git a/css/auction-website.css b/css/auction-website.css index 90cd0e2f..55ba077e 100644 --- a/css/auction-website.css +++ b/css/auction-website.css @@ -13,7 +13,7 @@ html * { border: none; } -#info-modal-img { +.modal-body > img { width: 100%; } diff --git a/index.html b/index.html index db4722b0..40bb7684 100644 --- a/index.html +++ b/index.html @@ -13,12 +13,8 @@ - - - + + @@ -31,12 +27,12 @@ @@ -67,14 +63,13 @@ - + - - diff --git a/js/auctions.js b/js/auctions.js index efd37797..9fa862f4 100644 --- a/js/auctions.js +++ b/js/auctions.js @@ -1,44 +1,43 @@ // Imports -import { auth, db, auctions } from "./firebase.js"; -import { bidModal } from "./popups.js"; -import { doc, getDoc, setDoc, updateDoc, writeBatch, onSnapshot } from "https://www.gstatic.com/firebasejs/9.20.0/firebase-firestore.js"; +import { auth, db } from "./firebase.js"; +import { doc, setDoc, updateDoc, writeBatch, onSnapshot } from "https://www.gstatic.com/firebasejs/9.20.0/firebase-firestore.js"; // For a real auction, set this to false let demoAuction = true; // Random auction information -function generateRandomAuctions() { - // Random cat images - document.querySelectorAll(".card > img").forEach((img, idx) => { - img.src = "https://cataas.com/cat/cute?random=" + idx; - auctions[idx].primaryImage = img.src; - auctions[idx].secondaryImage = img.src; - }); +function generateRandomAuctionData() { + let cards = document.querySelectorAll(".card") + // Random cat names $.getJSON( "https://random-data-api.com/api/name/random_name", { size: auctions.length }, - function (data) { - data.forEach((elem, idx) => { - document.querySelector("#auction-" + idx + " > div > h5").innerHTML = elem.name; - auctions[idx].title = elem.name; - }); + (data) => { + data.forEach((elem, i) => { + cards[i].querySelector(".title").innerText = elem.name + cards[i].dataset.bsTitle = elem.name + }); } ); // Random lorem ipsum cat descriptions $.getJSON( "https://random-data-api.com/api/lorem_ipsum/random_lorem_ipsum", { size: auctions.length }, - function (data) { - data.forEach((elem, idx) => { - document.querySelector("#auction-" + idx + " > div > p").innerHTML = elem.short_sentence; - auctions[idx].subtitle = elem.short_sentence; - auctions[idx].detail = elem.very_long_sentence; + (data) => { + data.forEach((elem, i) => { + cards[i].querySelector(".card-subtitle").innerText = elem.short_sentence + cards[i].dataset.bsSubtitle = elem.short_sentence; + cards[i].dataset.bsDetail = elem.very_long_sentence; }); } ); - // Random end times + // Random cat images and end times for (let i = 0; i < auctions.length; i++) { + cards[i].querySelector("img").src = "https://cataas.com/cat/cute?random=" + i; + cards[i].dataset.bsPrimaryImage = "https://cataas.com/cat/cute?random=" + i; + cards[i].dataset.bsSecondaryImage = "https://cataas.com/cat/cute?random=" + i; + let now = new Date(); let endTime = new Date().setHours(8 + i, 0, 0, 0) if (endTime - now < 0) { endTime = new Date(endTime).setDate(now.getDate() + 1) } @@ -90,81 +89,6 @@ export function setClocks() { setTimeout(setClocks, 1000); } -// Place a bid on an item -function placeBid() { - let nowTime = new Date().getTime(); - let modalBidButton = document.querySelector("#bid-modal > div > div > div.modal-footer > button.btn.btn-primary") - modalBidButton.setAttribute('disabled', '') // disable the button while we check - let i = modalBidButton.id.match("[0-9]+"); - let feedback = document.getElementById("bad-amount-feedback") - // Cleanse input - let amountElement = document.getElementById("amount-input") - let amount = Number(amountElement.value) - if (auctions[i].endTime - nowTime < 0) { - feedback.innerText = "The auction is already over!" - amountElement.classList.add("is-invalid") - setTimeout(() => { - bidModal.hide(); - amountElement.classList.remove("is-invalid"); - modalBidButton.removeAttribute('disabled', ''); - }, 1000); - } else if (amount == 0) { - // amount was empty - feedback.innerText = "Please specify an amount!" - amountElement.classList.add("is-invalid") - modalBidButton.removeAttribute('disabled', ''); - } else if (!(/^-?\d*\.?\d{0,2}$/.test(amount))) { - // field is does not contain money - feedback.innerText = "Please specify a valid amount!" - amountElement.classList.add("is-invalid") - modalBidButton.removeAttribute('disabled', ''); - } else { - // Checking bid amount - // Get item and user info - let user = auth.currentUser; - let itemId = i.toString().padStart(5, "0") - // Documents to check and write to - const liveRef = doc(db, "auction-live", "items"); - const storeRef = doc(db, "auction-store", itemId); - // Check live document - getDoc(liveRef).then(function (doc) { - console.log("Database read from placeBid()") - let thisItem = doc.data()[itemId]; - let bids = (Object.keys(thisItem).length - 1) / 2 - let currentBid = thisItem["bid" + bids] - if (amount >= 1 + currentBid) { - let keyStem = itemId + ".bid" + (bids + 1) - updateDoc(liveRef, { - [keyStem + "-uid"]: user.uid, - [keyStem]: amount, - }) - console.log("Database write from placeBid()") - let storeKey = "bid" + (bids + 1) - updateDoc(storeRef, { - [storeKey]: { - "bidder-username": user.displayName, - "bidder-uid": user.uid, - "amount": amount, - time: Date().substring(0, 24) - } - }) - console.log("Database write from placeBid()") - amountElement.classList.add("is-valid") - amountElement.classList.remove("is-invalid") - setTimeout(() => { - bidModal.hide(); - amountElement.classList.remove("is-valid"); - modalBidButton.removeAttribute('disabled', ''); - }, 1000); - } else { - amountElement.classList.add("is-invalid") - feedback.innerText = "You must bid at least £" + (currentBid + 1).toFixed(2) + "!" - modalBidButton.removeAttribute('disabled', ''); - } - }); - } -} - function argsort(array, key) { const arrayObject = array.map((value, idx) => { return { value, idx }; }); return arrayObject.sort((a, b) => (a.value[key] - b.value[key])); @@ -177,6 +101,10 @@ function generateAuctionCard(auction) { let card = document.createElement("div"); card.classList.add("card"); + card.dataset.bsTitle=auction.title + card.dataset.bsDetail=auction.detail + card.dataset.bsPrimaryImage=auction.primaryImage + card.dataset.bsSecondaryImage=auction.secondaryImage card.id = "auction-" + auction.idx col.appendChild(card); @@ -239,20 +167,18 @@ function generateAuctionCard(auction) { let infoButton = document.createElement("button"); infoButton.type = "button" - infoButton.href = "#"; infoButton.classList.add("btn", "btn-secondary") + infoButton.dataset.bsToggle="modal" + infoButton.dataset.bsTarget="#info-modal" infoButton.innerText = "Info"; - infoButton.onclick = function () { openInfo(this.id); } - infoButton.id = "info-button-" + auction.idx buttonGroup.appendChild(infoButton); let bidButton = document.createElement("button"); bidButton.type = "button" - bidButton.href = "#"; bidButton.classList.add("btn", "btn-primary") bidButton.innerText = "Submit bid"; - bidButton.onclick = function () { openBid(this.id); } - bidButton.id = "bid-button-" + auction.idx + bidButton.dataset.bsToggle="modal" + bidButton.dataset.bsTarget="#bid-modal" buttonGroup.appendChild(bidButton); return col @@ -266,7 +192,7 @@ export function populateAuctionGrid() { let auctionCard = generateAuctionCard(auction); auctionGrid.appendChild(auctionCard); }); - if (demoAuction) { generateRandomAuctions() }; + if (demoAuction) { generateRandomAuctionData() }; } function numberWithCommas(x) { @@ -348,7 +274,6 @@ function resetAll() { resetAllStore(); } -window.placeBid = placeBid window.resetAll = resetAll window.resetAllLive = resetAllLive window.resetAllStore = resetAllStore diff --git a/js/firebase.js b/js/firebase.js index 70ed5995..13d244dd 100644 --- a/js/firebase.js +++ b/js/firebase.js @@ -20,7 +20,7 @@ const app = initializeApp(firebaseConfig); export const db = getFirestore(app); export const auth = getAuth(app); -export const auctions = [ +const auctions = [ { primaryImage: "", title: "", @@ -117,4 +117,6 @@ export const auctions = [ secondaryImage: "", startingPrice: 7, endTime: 0 - }] \ No newline at end of file + }] + +window.auctions = auctions \ No newline at end of file diff --git a/js/popups.js b/js/popups.js index 29805557..74daacd8 100644 --- a/js/popups.js +++ b/js/popups.js @@ -1,68 +1,194 @@ // Imports -import { auth, db, auctions } from "./firebase.js"; -import { doc, setDoc } from "https://www.gstatic.com/firebasejs/9.20.0/firebase-firestore.js"; - -const loginModal = new bootstrap.Modal(document.getElementById('login-modal')) -const infoModal = new bootstrap.Modal(document.getElementById('info-modal')) -export const bidModal = new bootstrap.Modal(document.getElementById('bid-modal')) - -function openInfo(id) { - let i = id.match("[0-9]+"); - document.getElementById('info-modal-title').innerText = auctions[i].title - document.getElementById('info-modal-desc').innerHTML = auctions[i].detail - document.getElementById('info-modal-img').src = auctions[i].secondaryImage; - document.querySelector("#info-modal > div > div > div.modal-footer > button.btn.btn-primary").id = "info-modal-submit-bid-btn-" + i - infoModal.show() +import { auth, db } from "./firebase.js"; +import { doc, setDoc, getDoc, updateDoc } from "https://www.gstatic.com/firebasejs/9.20.0/firebase-firestore.js"; +import { signInAnonymously, onAuthStateChanged, updateProfile } from "https://www.gstatic.com/firebasejs/9.20.0/firebase-auth.js"; + +// -- Sign up modal and logic -- +const authButton = document.getElementById('auth-button') +const signUpModal = document.getElementById('login-modal') +const signUpModalObject = new bootstrap.Modal(signUpModal) +const signUpModalInput = signUpModal.querySelector('input') +const signUpModalSubmit = signUpModal.querySelector('.btn-primary') + +// Function called from index.html which creates anonymous account for user (or signs in if it already exists) +export function autoSignIn() { + onAuthStateChanged(auth, (user) => { + if (user && user.displayName != null) { + // If user has an anonymous account and a displayName, treat them as signed in + authButton.innerText = "Sign out" + document.getElementById('username-display').innerText = "Hi " + user.displayName + } else { + // Automatically create an anonymous account if user doesn't have one + signInAnonymously(auth) + } + }); } -function openBid(id) { - let i = id.match('[0-9]+'); - document.getElementById("amount-input").value = "" - document.getElementById("amount-input").classList.remove("is-invalid") - document.getElementById('bid-modal-subtitle').innerText = auctions[i].title - document.querySelector("#bid-modal > div > div > div.modal-footer > button.btn.btn-primary").id = "bid-modal-submit-bid-btn-" + i - let loggedIn = auth.currentUser && auth.currentUser.displayName - if (loggedIn) { - bidModal.show() - document.getElementById("amount-input").focus() +// Only shows signUpModal if the user is not signed in. Otherwise, it pretends to sign out +authButton.addEventListener("click", () => { + if (authButton.innerText == "Sign out") { + // Doesn't actually sign out, just gives the user the option to rename their account + authButton.innerText = "Sign in" + document.getElementById('username-display').innerText = "" } else { - openLogin() + signUpModalInput.value = "" + signUpModalObject.show() } -} +}) + +// Focus the username input once signUpModal is visible +signUpModal.addEventListener("shown.bs.modal", () => { signUpModalInput.focus() }) + +// Sign up can be triggered either by clicking the submit button or by pressing enter +signUpModalSubmit.addEventListener("click", () => { signUp() }) +signUpModalInput.addEventListener("keydown", (event) => { if (event.key == 'Enter') { signUp() } }) -function openLogin() { - let loggedIn = auth.currentUser && auth.currentUser.displayName - if (!loggedIn) { loginModal.show(); document.getElementById('username-input').focus() } +// Function that handles sign up logic +function signUp() { + let username = signUpModalInput + let user = auth.currentUser; + updateProfile(user, { displayName: username.value }) + setDoc(doc(db, "users", user.uid), { name: username.value, admin: false }) + authButton.innerText = "Sign out" + document.getElementById('username-display').innerText = "Hi " + username.value + username.classList.add("is-valid") + setTimeout(() => { + signUpModalObject.hide(); + username.classList.remove("is-valid"); + }, 1000); } -function newUserLogin() { - let loggedIn = auth.currentUser && auth.currentUser.displayName - if (!loggedIn) { - let username = document.getElementById('username-input').value +// --Bidding modal and logic -- +const bidModal = document.getElementById('bid-modal') +const bidModalObject = new bootstrap.Modal(bidModal) +const bidModalTitle = bidModal.querySelector("strong") +const bidModalInput = bidModal.querySelector("input") +const bidModalSubmit = bidModal.querySelector(".btn-primary") + +// Populate bidModal with the correct information before it is visible +bidModal.addEventListener("show.bs.modal", (event) => { + const button = event.relatedTarget + const card = button.closest(".card") || document.getElementById(bidModal.dataset.bsActiveAuction) + bidModalTitle.innerText = card.dataset.bsTitle + bidModal.dataset.bsActiveAuction = card.id + +}) + +// Focus the amount input once bidModal is visible +bidModal.addEventListener('shown.bs.modal', () => { + // If not logged in, open signUpModal instead + if (authButton.innerText == "Sign in") { + bidModalObject.hide() + signUpModalObject.show() + } else { + bidModalInput.focus() + } +}) + +// Once bidModal is no longer visible, clear the auction specific information +bidModal.addEventListener("hidden.bs.modal", () => { + bidModalInput.value = "" + bidModalInput.classList.remove("is-invalid") + bidModal.removeAttribute("data-bs-active-auction") +}) + +// A bid can be triggered either by clicking the submit button or by pressing enter +bidModalSubmit.addEventListener("click", () => { placeBid() }) +bidModalInput.addEventListener("keydown", (event) => { if (event.key == 'Enter') { placeBid() } }) + +// Function that handles bidding logic +function placeBid() { + let nowTime = new Date().getTime(); + bidModalSubmit.setAttribute('disabled', '') // disable the button while we check + let i = bidModal.dataset.bsActiveAuction.match("[0-9]+"); + let feedback = bidModal.querySelector(".invalid-feedback") + // Cleanse input + let amountElement = bidModal.querySelector("input") + let amount = Number(amountElement.value) + if (auctions[i].endTime - nowTime < 0) { + feedback.innerText = "The auction is already over!" + amountElement.classList.add("is-invalid") + setTimeout(() => { + bidModalObject.hide(); + amountElement.classList.remove("is-invalid"); + bidModalSubmit.removeAttribute('disabled', ''); + }, 1000); + } else if (amount == 0) { + // amount was empty + feedback.innerText = "Please specify an amount!" + amountElement.classList.add("is-invalid") + bidModalSubmit.removeAttribute('disabled', ''); + } else if (!(/^-?\d*\.?\d{0,2}$/.test(amount))) { + // field is does not contain money + feedback.innerText = "Please specify a valid amount!" + amountElement.classList.add("is-invalid") + bidModalSubmit.removeAttribute('disabled', ''); + } else { + // Checking bid amount + // Get item and user info let user = auth.currentUser; - user.updateProfile({ displayName: username }) - setDoc(doc(db, "users", user.uid), { name: username, admin: false }) - loginModal.hide() - replaceSignupButton(username) + let itemId = i.toString().padStart(5, "0") + // Documents to check and write to + const liveRef = doc(db, "auction-live", "items"); + const storeRef = doc(db, "auction-store", itemId); + // Check live document + getDoc(liveRef).then(function (doc) { + console.log("Database read from placeBid()") + let thisItem = doc.data()[itemId]; + let bids = (Object.keys(thisItem).length - 1) / 2 + let currentBid = thisItem["bid" + bids] + if (amount >= 1 + currentBid) { + let keyStem = itemId + ".bid" + (bids + 1) + updateDoc(liveRef, { + [keyStem + "-uid"]: user.uid, + [keyStem]: amount, + }) + console.log("Database write from placeBid()") + let storeKey = "bid" + (bids + 1) + updateDoc(storeRef, { + [storeKey]: { + "bidder-username": user.displayName, + "bidder-uid": user.uid, + "amount": amount, + time: Date().substring(0, 24) + } + }) + console.log("Database write from placeBid()") + amountElement.classList.add("is-valid") + amountElement.classList.remove("is-invalid") + setTimeout(() => { + bidModalObject.hide(); + amountElement.classList.remove("is-valid"); + bidModalSubmit.removeAttribute('disabled', ''); + }, 1000); + } else { + amountElement.classList.add("is-invalid") + feedback.innerText = "You must bid at least £" + (currentBid + 1).toFixed(2) + "!" + bidModalSubmit.removeAttribute('disabled', ''); + } + }); } } -export function autoLogin() { - auth.onAuthStateChanged(function (user) { - if (user && user.displayName != null) { - replaceSignupButton(user.displayName) - } else { - auth.signInAnonymously() - } - }); -} +// -- Info modal -- +const infoModal = document.getElementById('info-modal') -function replaceSignupButton(name) { - document.getElementById('signup-button').style.display = "none" - document.getElementById('username-display').innerText = "Hi " + name -} +// Populate infoModal with the correct information before it is visible +infoModal.addEventListener("show.bs.modal", (event) => { + const infoModalTitle = infoModal.querySelector('.modal-title') + const infoModalDetail = infoModal.querySelector('.modal-body > p') + const infoModalSecondaryImage = infoModal.querySelector('.modal-body > img') + // Update variable content elements + const button = event.relatedTarget + const card = button.closest(".card") + infoModalTitle.innerText = card.dataset.bsTitle + infoModalDetail.innerText = card.dataset.bsDetail + infoModalSecondaryImage.src = card.dataset.bsSecondaryImage + // Add the auction ID to the bidModal, in case the user clicks "Submit bid" in infoModal + bidModal.dataset.bsActiveAuction = card.id +}) -window.openLogin = openLogin -window.newUserLogin = newUserLogin -window.openBid = openBid -window.openInfo = openInfo +// Clear the auction specific information from bidModal when hiding infoModal +bidModal.addEventListener("hide.bs.modal", () => { + bidModal.removeAttribute("data-bs-active-auction") +}) \ No newline at end of file