complete badges

This commit is contained in:
Holly Stubbs 2024-10-08 11:04:46 +01:00
parent d83eef15c0
commit da80eb2d78
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
16 changed files with 73 additions and 21 deletions

View file

@ -120,11 +120,13 @@ export default class AdminController_Auth$Admin extends Controller {
adminBadgeViewModel.description = badge.Description; adminBadgeViewModel.description = badge.Description;
adminBadgeViewModel.imageUrl = badge.ImageUrl; adminBadgeViewModel.imageUrl = badge.ImageUrl;
adminBadgeViewModel.forUrl = badge.ForUrl; adminBadgeViewModel.forUrl = badge.ForUrl;
adminBadgeViewModel.isSecret = badge.IsSecret;
} else { } else {
adminBadgeViewModel.name = ""; adminBadgeViewModel.name = "";
adminBadgeViewModel.description = ""; adminBadgeViewModel.description = "";
adminBadgeViewModel.imageUrl = ""; adminBadgeViewModel.imageUrl = "";
adminBadgeViewModel.forUrl = ""; adminBadgeViewModel.forUrl = "";
adminBadgeViewModel.isSecret = false;
} }
return this.view(adminBadgeViewModel); return this.view(adminBadgeViewModel);
@ -135,7 +137,7 @@ export default class AdminController_Auth$Admin extends Controller {
return this.badRequest(); return this.badRequest();
} }
await BadgeService.SaveBadge(this.session.userId, parseInt(adminBadgeViewModel.id), adminBadgeViewModel.name ?? "", adminBadgeViewModel.description ?? "", adminBadgeViewModel.imageUrl ?? "", adminBadgeViewModel.forUrl ?? ""); await BadgeService.SaveBadge(this.session.userId, parseInt(adminBadgeViewModel.id), adminBadgeViewModel.name ?? "", adminBadgeViewModel.description ?? "", adminBadgeViewModel.imageUrl ?? "", adminBadgeViewModel.forUrl ?? "", (adminBadgeViewModel.isSecret?.toString() ?? "") === "on");
return this.redirectToAction("badges"); return this.redirectToAction("badges");
} }

View file

@ -1,7 +1,11 @@
import Controller from "./Controller"; import Controller from "./Controller";
import FunkyArray from "funky-array";
import UserService from "../services/UserService"; import UserService from "../services/UserService";
import HomeViewModel from "../models/home/HomeViewModel"; import HomeViewModel from "../models/home/HomeViewModel";
import PartyService from "../services/PartyService"; import PartyService from "../services/PartyService";
import BadgeService from "../services/BadgeService";
import Badge from "../entities/Badge";
import UserBadge from "../entities/UserBadge";
export default class HomeController extends Controller { export default class HomeController extends Controller {
public async Index_Get_AllowAnonymous() { public async Index_Get_AllowAnonymous() {
@ -14,12 +18,23 @@ export default class HomeController extends Controller {
const parties = await PartyService.GetUserParties(this.session.userId); const parties = await PartyService.GetUserParties(this.session.userId);
const activeUserParty = await UserService.GetActiveParty(this.session.userId); const activeUserParty = await UserService.GetActiveParty(this.session.userId);
const unlockedBadges = await UserService.LoadUserBadges(this.session.userId); const unlockedBadges = await UserService.LoadUserBadges(this.session.userId);
const unlockedBadgesById = new FunkyArray<number, UserBadge>();
for (const unlockedBadge of unlockedBadges) {
unlockedBadgesById.set(unlockedBadge.BadgeId, unlockedBadge);
}
const badges = await BadgeService.LoadAll();
const badgeById = new FunkyArray<number, Badge>();
for (const badge of badges) {
badgeById.set(badge.Id, badge);
}
const homeViewModel: HomeViewModel = { const homeViewModel: HomeViewModel = {
user, user,
parties, parties,
activeUserParty, activeUserParty,
unlockedBadges unlockedBadges,
unlockedBadgesById,
badgeById
}; };
return this.view("home", homeViewModel); return this.view("home", homeViewModel);

View file

@ -4,6 +4,7 @@ export default class Badge {
public Description: string; public Description: string;
public ImageUrl: string; public ImageUrl: string;
public ForUrl: string; public ForUrl: string;
public IsSecret: boolean;
public CreatedByUserId: number; public CreatedByUserId: number;
public CreatedDatetime: Date; public CreatedDatetime: Date;
public LastModifiedByUserId?: number; public LastModifiedByUserId?: number;
@ -18,6 +19,7 @@ export default class Badge {
this.Description = ""; this.Description = "";
this.ImageUrl = ""; this.ImageUrl = "";
this.ForUrl = ""; this.ForUrl = "";
this.IsSecret = false;
this.CreatedByUserId = Number.MIN_VALUE; this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0); this.CreatedDatetime = new Date(0);
this.IsDeleted = false; this.IsDeleted = false;

View file

@ -3,5 +3,6 @@ export default interface AdminBadgeViewModel {
name?: string, name?: string,
description?: string, description?: string,
imageUrl?: string, imageUrl?: string,
forUrl?: string forUrl?: string,
isSecret?: boolean
} }

View file

@ -1,3 +1,5 @@
import Badge from "../../entities/Badge";
import FunkyArray from "funky-array";
import Party from "../../entities/Party"; import Party from "../../entities/Party";
import User from "../../entities/User"; import User from "../../entities/User";
import UserBadge from "../../entities/UserBadge"; import UserBadge from "../../entities/UserBadge";
@ -7,5 +9,7 @@ export default interface HomeViewModel {
user: User, user: User,
parties: Array<Party>, parties: Array<Party>,
activeUserParty: UserParty | null, activeUserParty: UserParty | null,
unlockedBadges: Array<UserBadge> unlockedBadges: Array<UserBadge>,
unlockedBadgesById: FunkyArray<number, UserBadge>,
badgeById: FunkyArray<number, Badge>
} }

View file

@ -35,12 +35,12 @@ export default abstract class BadgeRepo {
public static async insertUpdate(badge:Badge) { public static async insertUpdate(badge:Badge) {
if (badge.Id === Number.MIN_VALUE) { if (badge.Id === Number.MIN_VALUE) {
badge.Id = (await Database.Instance.query("INSERT Badge (Name, Description, ImageUrl, ForUrl, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [ badge.Id = (await Database.Instance.query("INSERT Badge (Name, Description, ImageUrl, ForUrl, IsSecret, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [
badge.Name, badge.Description, badge.ImageUrl, badge.ForUrl, badge.CreatedByUserId, badge.CreatedDatetime.getTime(), badge.LastModifiedByUserId ?? null, badge.LastModifiedDatetime?.getTime() ?? null, badge.DeletedByUserId ?? null, badge.DeletedDatetime?.getTime() ?? null, Number(badge.IsDeleted) badge.Name, badge.Description, badge.ImageUrl, badge.ForUrl, Number(badge.IsSecret), badge.CreatedByUserId, badge.CreatedDatetime.getTime(), badge.LastModifiedByUserId ?? null, badge.LastModifiedDatetime?.getTime() ?? null, badge.DeletedByUserId ?? null, badge.DeletedDatetime?.getTime() ?? null, Number(badge.IsDeleted)
]))[0]["Id"]; ]))[0]["Id"];
} else { } else {
await Database.Instance.query(`UPDATE Badge SET Name = ?, Description = ?, ImageUrl = ?, ForUrl = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [ await Database.Instance.query(`UPDATE Badge SET Name = ?, Description = ?, ImageUrl = ?, ForUrl = ?, IsSecret = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [
badge.Name, badge.Description, badge.ImageUrl, badge.ForUrl, badge.CreatedByUserId, badge.CreatedDatetime.getTime(), badge.LastModifiedByUserId ?? null, badge.LastModifiedDatetime?.getTime() ?? null, badge.DeletedByUserId ?? null, badge.DeletedDatetime?.getTime() ?? null, Number(badge.IsDeleted), badge.Id badge.Name, badge.Description, badge.ImageUrl, badge.ForUrl, Number(badge.IsSecret), badge.CreatedByUserId, badge.CreatedDatetime.getTime(), badge.LastModifiedByUserId ?? null, badge.LastModifiedDatetime?.getTime() ?? null, badge.DeletedByUserId ?? null, badge.DeletedDatetime?.getTime() ?? null, Number(badge.IsDeleted), badge.Id
]); ]);
} }
@ -54,11 +54,12 @@ function populateBadgeFromDB(badge:Badge, dbBadge:any) {
badge.Description = dbBadge.Description; badge.Description = dbBadge.Description;
badge.ImageUrl = dbBadge.ImageUrl; badge.ImageUrl = dbBadge.ImageUrl;
badge.ForUrl = dbBadge.ForUrl; badge.ForUrl = dbBadge.ForUrl;
badge.IsSecret = dbBadge.IsSecret[0] === 1;
badge.CreatedByUserId = dbBadge.CreatedByUserId; badge.CreatedByUserId = dbBadge.CreatedByUserId;
badge.CreatedDatetime = new Date(dbBadge.CreatedDatetime); badge.CreatedDatetime = new Date(dbBadge.CreatedDatetime);
badge.LastModifiedByUserId = dbBadge.LastModifiedByUserId; badge.LastModifiedByUserId = dbBadge.LastModifiedByUserId;
badge.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbBadge.LastModifiedDatetime); badge.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbBadge.LastModifiedDatetime);
badge.DeletedByUserId = dbBadge.DeletedByUserId; badge.DeletedByUserId = dbBadge.DeletedByUserId;
badge.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbBadge.DeletedDatetime); badge.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbBadge.DeletedDatetime);
badge.IsDeleted = dbBadge.IsDeleted === 1; badge.IsDeleted = dbBadge.IsDeleted[0] === 1;
} }

View file

@ -86,5 +86,5 @@ function populatePartyFromDB(party:Party, dbParty:any) {
party.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbParty.LastModifiedDatetime); party.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbParty.LastModifiedDatetime);
party.DeletedByUserId = dbParty.DeletedByUserId; party.DeletedByUserId = dbParty.DeletedByUserId;
party.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbParty.DeletedDatetime); party.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbParty.DeletedDatetime);
party.IsDeleted = dbParty.IsDeleted === 1; party.IsDeleted = dbParty.IsDeleted[0] === 1;
} }

View file

@ -15,7 +15,7 @@ export default abstract class UserBadgeRepo {
} }
public static async selectByUserId(userId:number) { public static async selectByUserId(userId:number) {
const dbUserBadge = await Database.Instance.query("SELECT * FROM UserBadge WHERE UserId = ? AND IsDeleted = 0 LIMIT 1", [ userId ]); const dbUserBadge = await Database.Instance.query("SELECT * FROM UserBadge WHERE UserId = ? AND IsDeleted = 0", [ userId ]);
const userBadges = new Array<UserBadge>(); const userBadges = new Array<UserBadge>();
for (const row of dbUserBadge) { for (const row of dbUserBadge) {
@ -63,5 +63,5 @@ function populateUserBadgeFromDB(userBadge:UserBadge, dbUser:any) {
userBadge.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.LastModifiedDatetime); userBadge.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.LastModifiedDatetime);
userBadge.DeletedByUserId = dbUser.DeletedByUserId; userBadge.DeletedByUserId = dbUser.DeletedByUserId;
userBadge.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.DeletedDatetime); userBadge.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.DeletedDatetime);
userBadge.IsDeleted = dbUser.IsDeleted === 1; userBadge.IsDeleted = dbUser.IsDeleted[0] === 1;
} }

View file

@ -84,5 +84,5 @@ function populateUserPartyFromDB(userParty:UserParty, dbUserParty:any) {
userParty.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUserParty.LastModifiedDatetime); userParty.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUserParty.LastModifiedDatetime);
userParty.DeletedByUserId = dbUserParty.DeletedByUserId; userParty.DeletedByUserId = dbUserParty.DeletedByUserId;
userParty.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUserParty.DeletedDatetime); userParty.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUserParty.DeletedDatetime);
userParty.IsDeleted = dbUserParty.IsDeleted === 1; userParty.IsDeleted = dbUserParty.IsDeleted[0] === 1;
} }

View file

@ -77,12 +77,12 @@ function populateUserFromDB(user:User, dbUser:any) {
user.PasswordHash = dbUser.PasswordHash; user.PasswordHash = dbUser.PasswordHash;
user.PasswordSalt = dbUser.PasswordSalt; user.PasswordSalt = dbUser.PasswordSalt;
user.APIKey = dbUser.APIKey; user.APIKey = dbUser.APIKey;
user.HasUsedClient = dbUser.HasUsedClient === 1; user.HasUsedClient = dbUser.HasUsedClient[0] === 1;
user.CreatedByUserId = dbUser.CreatedByUserId; user.CreatedByUserId = dbUser.CreatedByUserId;
user.CreatedDatetime = new Date(dbUser.CreatedDatetime); user.CreatedDatetime = new Date(dbUser.CreatedDatetime);
user.LastModifiedByUserId = dbUser.LastModifiedByUserId; user.LastModifiedByUserId = dbUser.LastModifiedByUserId;
user.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.LastModifiedDatetime); user.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.LastModifiedDatetime);
user.DeletedByUserId = dbUser.DeletedByUserId; user.DeletedByUserId = dbUser.DeletedByUserId;
user.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.DeletedDatetime); user.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.DeletedDatetime);
user.IsDeleted = dbUser.IsDeleted === 1; user.IsDeleted = dbUser.IsDeleted[0] === 1;
} }

View file

@ -4,7 +4,7 @@ import BadgeRepo from "../repos/BadgeRepo";
import BadgeCache from "../objects/BadgeCache"; import BadgeCache from "../objects/BadgeCache";
export default abstract class BadgeService { export default abstract class BadgeService {
public static async SaveBadge(currentUserId:number, id:number | undefined, name:string, description:string, imageUrl: string, forUrl:string) { public static async SaveBadge(currentUserId:number, id:number | undefined, name:string, description:string, imageUrl: string, forUrl:string, isSecret: boolean) {
try { try {
let badge = id ? await BadgeRepo.selectById(id) : null; let badge = id ? await BadgeRepo.selectById(id) : null;
if (badge === null) { if (badge === null) {
@ -16,10 +16,13 @@ export default abstract class BadgeService {
badge.LastModifiedDatetime = new Date(); badge.LastModifiedDatetime = new Date();
} }
console.log(isSecret);
badge.Name = name; badge.Name = name;
badge.Description = description; badge.Description = description;
badge.ImageUrl = imageUrl; badge.ImageUrl = imageUrl;
badge.ForUrl = forUrl; badge.ForUrl = forUrl;
badge.IsSecret = isSecret;
badge = await BadgeRepo.insertUpdate(badge); badge = await BadgeRepo.insertUpdate(badge);

View file

@ -42,12 +42,20 @@
<img id="imageImg" style="image-rendering:pixelated;margin-top: 36px" class="mb-3" src="<%= imageUrl.trim().length === 0 ? "/img/missing.png" : imageUrl %>" onerror="if (this.src != '/img/missing.png') this.src = '/img/missing.png';" width="32" height="32" /> <img id="imageImg" style="image-rendering:pixelated;margin-top: 36px" class="mb-3" src="<%= imageUrl.trim().length === 0 ? "/img/missing.png" : imageUrl %>" onerror="if (this.src != '/img/missing.png') this.src = '/img/missing.png';" width="32" height="32" />
</div> </div>
</div> </div>
<div class="row mb-5"> <div class="row mb-3">
<div class="col"> <div class="col">
<label for="forUrl" class="form-label">For URL</label> <label for="forUrl" class="form-label">For URL</label>
<input class="form-control" id="forUrl" name="forUrl" value="<%= typeof(forUrl) === "undefined" ? "" : forUrl %>" required /> <input class="form-control" id="forUrl" name="forUrl" value="<%= typeof(forUrl) === "undefined" ? "" : forUrl %>" required />
</div> </div>
</div> </div>
<div class="row mb-5">
<div class="col">
<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="isSecret" name="isSecret" <%= isSecret ? "checked" : "" %> />
<label for="isSecret" class="form-check-label">Is Secret</label>
</div>
</div>
</div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col text-center"> <div class="col text-center">
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary">Save</button>

View file

@ -27,6 +27,7 @@
<th>&nbsp;</th> <th>&nbsp;</th>
<th>Name</th> <th>Name</th>
<th>For URL</th> <th>For URL</th>
<th style="white-space:nowrap">Secret</th>
<th>&nbsp;</th> <th>&nbsp;</th>
</thead> </thead>
<tbody> <tbody>
@ -35,6 +36,7 @@
<td class="align-middle"><img style="image-rendering:pixelated" src="<%= badge.ImageUrl.trim().length === 0 ? "/img/missing.png" : badge.ImageUrl %>" width="32" height="32" /></td> <td class="align-middle"><img style="image-rendering:pixelated" src="<%= badge.ImageUrl.trim().length === 0 ? "/img/missing.png" : badge.ImageUrl %>" width="32" height="32" /></td>
<td class="align-middle"><%= badge.Name %></td> <td class="align-middle"><%= badge.Name %></td>
<td class="align-middle"><a href="<%= badge.ForUrl %>"><%= badge.ForUrl %></a></td> <td class="align-middle"><a href="<%= badge.ForUrl %>"><%= badge.ForUrl %></a></td>
<td class="align-middle text-center"><%= badge.IsSecret ? "Yes" : "No" %></td>
<td class="text-end text-nowrap align-middle"> <td class="text-end text-nowrap align-middle">
<a class="btn btn-sm btn-primary" href="/admin/badge?id=<%= badge.Id %>">Edit</a> <a class="btn btn-sm btn-primary" href="/admin/badge?id=<%= badge.Id %>">Edit</a>
<a class="btn btn-sm btn-danger" href="/admin/deletebadge?id=<%= badge.Id %>" onclick="return confirm(`Are you sure you want to delete '<%= badge.Name %>'?`)">Delete</a> <a class="btn btn-sm btn-danger" href="/admin/deletebadge?id=<%= badge.Id %>" onclick="return confirm(`Are you sure you want to delete '<%= badge.Name %>'?`)">Delete</a>

View file

@ -1,5 +1,4 @@
</div> </div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js" integrity="sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cookieconsent/3.1.1/cookieconsent.min.js" integrity="sha512-yXXqOFjdjHNH1GND+1EO0jbvvebABpzGKD66djnUfiKlYME5HGMUJHoCaeE4D5PTG2YsSJf6dwqyUUvQvS0vaA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/cookieconsent/3.1.1/cookieconsent.min.js" integrity="sha512-yXXqOFjdjHNH1GND+1EO0jbvvebABpzGKD66djnUfiKlYME5HGMUJHoCaeE4D5PTG2YsSJf6dwqyUUvQvS0vaA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script> <script>
(() => { (() => {

View file

@ -18,6 +18,7 @@
</style> </style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js" integrity="sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head> </head>
<body> <body>
<nav class="navbar navbar-expand bg-body-tertiary"> <nav class="navbar navbar-expand bg-body-tertiary">

View file

@ -24,7 +24,7 @@
<% } %> <% } %>
</div> </div>
</div> </div>
<div class="col"> <div class="col pb-5">
<h3>Your Parties</h3> <h3>Your Parties</h3>
<% if (parties.length > 0) { %> <% if (parties.length > 0) { %>
<table class="table table-striped"> <table class="table table-striped">
@ -66,11 +66,25 @@
<h3>Badges</h3> <h3>Badges</h3>
<div class="row mt-4"> <div class="row mt-4">
<div class="col"> <div class="col">
<% for (let i = 0; i < 71; i++) { %> <% for (const badgeKey of badgeById.keys) { %>
<img class="mb-3" src="https://eusv.net/Mu6LedXkxrZDUB"> <% const badge = badgeById.get(badgeKey); const unlockedBadge = unlockedBadgesById.get(badgeKey); %>
<% if (unlockedBadge) { %>
<span class="d-inline-block mb-3" tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-placement="top" data-bs-title="<%= badge.Name %>" data-bs-content="<%= badge.Description %><br><small>Unlocked on <%= badge.CreatedDatetime.toString().split(" ").slice(1, 4).join(" ") %></small>">
<img width="32" height="32" src="<%= badge.ImageUrl %>">
</span>
<% } else if (!badge.IsSecret) { %>
<span class="d-inline-block mb-3" tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-placement="top" data-bs-title="<%= badge.Name %>" data-bs-content="<small>Not yet unlocked</small>">
<img src="https://eusv.net/Mu6LedXkxrZDUB">
</span>
<% } %>
<% } %> <% } %>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script>
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl, { html: true }));
</script>
<%- include("../base/footer") %> <%- include("../base/footer") %>