Skip to content

Commit

Permalink
Add admin dashboard (#25)
Browse files Browse the repository at this point in the history
* Add title to bid0 of all auctions

* Add admin page and links between the two pages

* Remove unecessary css

* Remove Bootstrap prefixes from data attributes

* Admin btn only visible if user doc has admin field

* Populate winning column with usernames

* Add debug log to show user read

* Add is to admin button on admin page

* Move admin functions to `admin.js`

* Update README.md

* Fix table CSS on main page

* Move setup logic out of html files

* Refactor reset functions to use `getItems()`

* Move generateRandomAuctionData data to `demo.js`

* Convert everything to data attributes

* Document changes

* Remove missed bootstrap prefix
  • Loading branch information
hmellor authored Apr 23, 2023
1 parent 7c48f82 commit be4793d
Show file tree
Hide file tree
Showing 10 changed files with 439 additions and 240 deletions.
31 changes: 16 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,28 @@ This is a project I worked on for a charity as a pet project and so the function
- Device based login requiring only a username to be provided (no need to store sensitive information).
- Popups for more detailed descriptions and additional imagery.
- Realtime bidding using event listeners (no need to refresh page).
- Built with Bootstrap so everything is reactive.
- Has a separate page for administrators to monitor the auction.

![](./readme/homepage_desktop.png) ![](./readme/loginpage.png)

![](./readme/infopage.png) ![](./readme/bidpage.png)

It looks great on mobile too!

![](./readme/homepage_mobile.png)
| ![](./readme/homepage_desktop.png) | ![](./readme/loginpage.png) |
|:---:|:---:|
| ![](./readme/infopage.png) | ![](./readme/bidpage.png) |
| <img src="./readme/homepage_mobile.png" width="50%"> | ![](./readme/adminpage.png) |

## Setup
Here we will cover how to add your own information to the auctions themselves, then how to most a local server to see your changes and finally how to connect it all to Firebase to enable user login and bidding.

### Adding auction information
First, set `demoAuction=False` in `js/auctions.js` (this will keep the cats at bay).
First, set `isDemo=False` in `js/auctions.js` (this will keep the cats at bay).

Then, populate the `Object` at the bottom of `js/firebase.js` with the information for of the items you'll be putting up for auction. The fields are:
- `primaryImage` (`String`): path or URL to the primary image
- `title` (`String`): auction title
- `subtitle` (`String`): auction subtitle
- `detail` (`String`): auction detail text
- `secondaryImage` (`String`): path or URL to the secondary image
- `startingPrice` (`Number`): auction price,
- `startPrice` (`Number`): auction price,
- `endTime` (`Number`): auction end time relative to epoch **in milliseconds**. (See [JavaScript's `Date` class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) for more information.)

### Firebase setup
Expand Down Expand Up @@ -105,18 +105,19 @@ Setting up the database is a little more involved so here are the steps you must
- Admins can access all auction and user data.

### Creating an admin account and initialising your auctions
The final step in setting up your auction is to create an admin account and use it to initialise your auctions.
The final step in setting up your auction is to create an admin account and use it to initialise and monitor your auction.

To create an admin account:
- Host your website either locally or on GitHub Pages and log in to your website.
- Host your website either locally or on GitHub Pages and log in to the website.
- Then go to your Firestore console and find the document for the user you just created.
- There should be a field in the document called `admin` which has the value `"false"`. You must now create a password (or hash) that enables admin access and set the `admin` field to this value.
- Go to your Firestore rules and replace `"insert long random secret string"` with your admin password.
- You have now created your admin account.
- There should be a boolean field in the document called `admin` which has the value `"false"`. Change its type to string and enter your desired admin password*. The longer and more complicated the password the better, consider using something like an MD5 hash. You'll never actually have to enter it anywhere, it's just used when validating your Firestore requests.
- Go to your Firestore rules and replace `insert long random secret string` with the password you **just created**.

> *_**Please** don't reuse one of your existing passwords! While the Firestore rules should prevent bad actors from reading your user's data, don't risk it. I can't be responsible for leaked passwords due to a misconfigured project_
To initialise the auctions:
- With the device you used to create your admin account, head to your website.
- With the device you used to create your admin account, head to your website and navigate to the admin page by clicking the `Admin` button in the top right.
- Open the developer console (F12) and enter `resetAll()` into the console.
- This will revert the entire auction to the initial state specified in `js/firebase.js` (as long as you are admin), be careful with this one!
- You can also reset individual items using the `resetItem(i)` function.
- Your auction is now ready.
- You can also use this `Admin` page to monitor the status of your auction.
108 changes: 108 additions & 0 deletions admin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<!doctype html>
<html class="no-js" lang="en">

<head>
<meta charset="utf-8" />
<meta name="robots" content="NOINDEX, NOFOLLOW">
<!--add no follow tag to keep out of front facing index and search engines-->
<meta name="description" content="An auction website">
<meta property="og:image" content="https://www.mellor.io/auction-website/img/banner.png">
<meta name="keywords" content="Online Auctions">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Markatplace</title>
<link rel="icon" type="image/png" href="./img/favicon.ico">

<!-- Bootstrap -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe"
crossorigin="anonymous"></script>

<!-- Custom CSS -->
<link rel="stylesheet" href="./css/auction-website.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
</head>

<body>
<!-- Navbar -->
<nav class="navbar navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand mb-0 h1 me-auto">
<img src="./img/logo.png" alt="" width="30" height="24" class="d-inline-block align-text-top">
The Markatplace
</a>
<div class="row row-cols-auto">
<a class="navbar-brand" id="username-display"></a>
<a id="admin-button" class="btn btn-secondary me-2" href="index.html" role="button">Main</a>
<button id="auth-button" class="btn btn-secondary me-2">Sign up</button>
</div>
</div>
</nav>
<!-- Grid of auction items -->
<div id="table-container" class="container">
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Title</th>
<th scope="col">Price</th>
<th scope="col">Bids</th>
<th scope="col">Winning</th>
<th scope="col">Time left</th>
</tr>
</thead>
<tbody></tbody>
</table>

<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
<div class="col-md-4 d-flex align-items-center">
<span class="text-muted">© 2022 Harry Mellor</span>
</div>

<ul class="nav col-md-4 justify-content-end list-unstyled d-flex">
<li class="ms-3"><a class="bi bi-github text-muted" href="https://github.com/HMellor/" width="24"
height="24"></a></li>
</ul>
</footer>
</div>

<!-- Login popup -->
<div id="login-modal" class="modal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 id="login-modal-title" class="modal-title">Sign up for Markatplace Auction</h5>
</div>
<div class="modal-body">
<p>We use anonymous authentication provided by Google. Your account is attached to your device signature.</p>
<p>The username just lets us know who's bidding!</p>
<form onsubmit="return false;">
<div class="form-floating mb-3">
<input type="username" class="form-control" id="username-input" placeholder="username">
<label for="username-input">Username</label>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"
aria-label="Cancel">Cancel</button>
<button type="submit" class="btn btn-primary">Sign up</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<!-- Create anonymous account -->
<script type="module">
import { autoSignIn } from "./js/popups.js";
import { setupTable } from "./js/admin.js";
autoSignIn();
setupTable();
</script>
</body>

</html>
2 changes: 1 addition & 1 deletion css/auction-website.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ html * {
width: 100%;
}

.table {
.card > .table {
margin-bottom: 0;
}
13 changes: 7 additions & 6 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@
<img src="./img/logo.png" alt="" width="30" height="24" class="d-inline-block align-text-top">
The Markatplace
</a>
<a class="navbar-brand" id="username-display"></a>
<button id="auth-button" class="btn btn-secondary">Sign up</button>
<div class="row row-cols-auto">
<a class="navbar-brand" id="username-display"></a>
<a id="admin-button" class="btn btn-secondary me-2" href="admin.html" role="button" style="display: none">Admin</a>
<button id="auth-button" class="btn btn-secondary me-2">Sign up</button>
</div>
</div>
</nav>
<!-- Grid of auction items -->
Expand Down Expand Up @@ -124,12 +127,10 @@ <h5 class="modal-title">Place your bid</h5>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<!-- Create anonymous account -->
<script type="module">
import { populateAuctionGrid, setClocks, dataListener } from "./js/auctions.js";
import { autoSignIn } from "./js/popups.js";
populateAuctionGrid();
setClocks();
dataListener()
import { setupItems } from "./js/auctions.js";
autoSignIn();
setupItems();
</script>
</body>

Expand Down
109 changes: 109 additions & 0 deletions js/admin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Imports
import { db } from "./firebase.js";
import { timeBetween, getItems } from "./auctions.js";
import { doc, setDoc, getDoc, updateDoc, deleteField, onSnapshot } from "https://www.gstatic.com/firebasejs/9.20.0/firebase-firestore.js";

let table = document.querySelector("tbody");

function createRow(item) {
let row = document.createElement("tr")
row.id = `auction-${item}`

let header = document.createElement("th")
header.scope = "row"
header.innerText = item
row.appendChild(header)

row.appendChild(document.createElement("td"))
row.appendChild(document.createElement("td"))
row.appendChild(document.createElement("td"))
row.appendChild(document.createElement("td"))
row.appendChild(document.createElement("td"))

return row
}

function dataListener() {
// Listen for updates in active auctions
onSnapshot(doc(db, "auction", "items"), (items) => {
console.debug("dataListener() read from auction/items")
// Parse flat document data into structured Object
let data = {}
for (const [key, details] of Object.entries(items.data())) {
let [item, bid] = key.split("_").map(i => Number(i.match(/\d+/)))
data[item] = data[item] || {}
data[item][bid] = details
}
// Use structured Object to populate the row for each item
for (const [item, bids] of Object.entries(data)) {
let row = table.querySelector(`#auction-${item}`)
if (row == null) {
row = createRow(item)
table.appendChild(row)
}
// Extract bid data
let bidCount = Object.keys(bids).length - 1
row.children[1].innerText = bids[0].title
row.children[2].innerText = ${bids[bidCount].amount.toFixed(2)}`
row.children[3].innerText = bidCount
if (bids[bidCount].uid) {
getDoc(doc(db, "users", bids[bidCount].uid)).then((user) => {
row.children[4].innerText = user.get("name")
console.debug("dataListener() read from users")
})
} else {
// Remove winner name if auction was reset
row.children[4].innerText = ""
}
row.children[5].dataset.endTime = bids[0].endTime
}
})
}

function setClocks() {
let now = new Date().getTime()
document.querySelectorAll("tbody > tr").forEach(row => {
row.children[5].innerText = timeBetween(now, row.children[5].dataset.endTime);
})
setTimeout(setClocks, 1000);
}

export function setupTable() {
dataListener();
setClocks();
}

function resetItem(i) {
const docRef = doc(db, "auction", "items")
getItems().then(items => {
let initialState = {}
getDoc(docRef).then((doc) => {
console.debug("resetItem() read from auction/items")
// Find all bids for item i
let item = items[i]
let keys = Object.keys(doc.data()).sort()
keys.filter(key => key.includes(`item${i.toString().padStart(5, "0")}`)).forEach((key, idx) => {
// Mark all except bid00000 to be deleted
initialState[key] = idx ? deleteField() : { amount: item.startPrice, title: item.title, endTime: item.endTime }
})
}).then(() => {
updateDoc(docRef, initialState)
console.debug("resetItem() write to from auction/items")
})
})
}

function resetAll() {
getItems().then(items => {
let initialState = {}
items.forEach(item => {
let field = `item${item.id.toString().padStart(5, "0")}_bid00000`
initialState[field] = { amount: item.startPrice, title: item.title, endTime: item.endTime }
})
setDoc(doc(db, "auction", "items"), initialState)
console.debug("resetAll() write to auction/items")
})
}

window.resetItem = resetItem
window.resetAll = resetAll
Loading

0 comments on commit be4793d

Please sign in to comment.