forked from Hanabi/hanabi-league
Add stat pages for variants
This commit is contained in:
parent
f5d3cfdad4
commit
488b38c7f3
6 changed files with 371 additions and 14 deletions
74
css/leaderboards.css
Normal file
74
css/leaderboards.css
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
.player-name {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.alt-name {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
.score {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #17a2b8; /* Bootstrap's teal color */
|
||||||
|
}
|
||||||
|
.table td, .table th {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
color: #343a40; /* Bootstrap's dark color */
|
||||||
|
font-size: 2.0rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.alt-name {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #777;
|
||||||
|
margin-top: 0.25rem; /* reduce spacing above alt-name */
|
||||||
|
}
|
||||||
|
.score-large {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #17a2b8; /* Bootstrap's 'info' color */
|
||||||
|
}
|
||||||
|
.btn-link {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none; /* Remove the underline */
|
||||||
|
}
|
||||||
|
.btn-link:hover {
|
||||||
|
color: #0056b3;
|
||||||
|
text-decoration: none; /* Keep the underline removed when hovering */
|
||||||
|
}
|
||||||
|
.btn-link:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.variant-rating {
|
||||||
|
/*font-size: 1.2rem;*/
|
||||||
|
font-weight: bold;
|
||||||
|
color: #17a2b8; /* Bootstrap's teal color */
|
||||||
|
}
|
||||||
|
.stat-description {
|
||||||
|
display: inline-block;
|
||||||
|
width: 19em;
|
||||||
|
}
|
||||||
|
.history-bullets {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: 100%;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
.history-bullets ul {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
77
css/style.css
Normal file
77
css/style.css
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
@import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap");
|
||||||
|
body {
|
||||||
|
background: #f9f9f9;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding-top: 100px;
|
||||||
|
padding-bottom: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaderboard-card {
|
||||||
|
background: #fff;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.leaderboard-card.leaderboard-card--first {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
.leaderboard-card.leaderboard-card--first .leaderboard-card__top {
|
||||||
|
background: linear-gradient(45deg, #7e57c2, #ab47bc);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.leaderboard-card__top {
|
||||||
|
background: #f9f6ff;
|
||||||
|
padding: 20px 0 30px 0;
|
||||||
|
}
|
||||||
|
.leaderboard-card__body {
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: -40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.circle-img {
|
||||||
|
height: 70px;
|
||||||
|
width: 70px;
|
||||||
|
border-radius: 70px;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
img.circle-img.circle-img--small {
|
||||||
|
height: 55px;
|
||||||
|
width: 55px;
|
||||||
|
border-radius: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
border-spacing: 0 15px;
|
||||||
|
border-collapse: separate;
|
||||||
|
}
|
||||||
|
.table thead tr th,
|
||||||
|
.table thead tr td,
|
||||||
|
.table tbody tr th,
|
||||||
|
.table tbody tr td {
|
||||||
|
vertical-align: middle;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.table thead tr th:nth-last-child(1),
|
||||||
|
.table thead tr td:nth-last-child(1),
|
||||||
|
.table tbody tr th:nth-last-child(1),
|
||||||
|
.table tbody tr td:nth-last-child(1) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.table tbody tr {
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.table tbody tr td {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.table tbody tr td:nth-child(1) {
|
||||||
|
border-radius: 5px 0 0 5px;
|
||||||
|
}
|
||||||
|
.table tbody tr td:nth-last-child(1) {
|
||||||
|
border-radius: 0 5px 5px 0;
|
||||||
|
}
|
|
@ -35,7 +35,7 @@ k-factor:
|
||||||
num_early_games: 30
|
num_early_games: 30
|
||||||
high_rating: 1700
|
high_rating: 1700
|
||||||
|
|
||||||
min_suits: 4
|
min_suits: 5
|
||||||
max_suits: 6
|
max_suits: 6
|
||||||
|
|
||||||
# Corresponds to game IDs from hanab.live
|
# Corresponds to game IDs from hanab.live
|
||||||
|
|
|
@ -28,11 +28,43 @@ class Leader:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class VariantStats:
|
class VariantStats:
|
||||||
name: str
|
|
||||||
num_players: int
|
|
||||||
rating: int
|
rating: int
|
||||||
games_played: int
|
games_played: int
|
||||||
games_won: int
|
games_won: int
|
||||||
|
total_bdr: int
|
||||||
|
total_crits_lost: int
|
||||||
|
total_moves: int
|
||||||
|
|
||||||
|
@property
|
||||||
|
def winrate(self):
|
||||||
|
if self.games_played == 0:
|
||||||
|
return 0
|
||||||
|
return round(100 * float(self.games_won) / self.games_played, 1)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def average_bdr(self):
|
||||||
|
if self.games_played == 0:
|
||||||
|
return 0
|
||||||
|
return round(float(self.total_bdr) / self.games_played, 3)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def average_crits_lost(self):
|
||||||
|
if self.games_played == 0:
|
||||||
|
return 0
|
||||||
|
return round(float(self.total_crits_lost) / self.games_played, 3)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def average_moves(self):
|
||||||
|
if self.games_played == 0:
|
||||||
|
return 0
|
||||||
|
return round(float(self.total_moves) / self.games_played, 3)
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VariantRow:
|
||||||
|
variant_id: int
|
||||||
|
name: str
|
||||||
|
num_players: int
|
||||||
|
stats: VariantStats
|
||||||
|
|
||||||
|
|
||||||
def get_rating_lists() -> Dict[int, List[PlayerEntry]]:
|
def get_rating_lists() -> Dict[int, List[PlayerEntry]]:
|
||||||
|
@ -120,15 +152,19 @@ def get_stat_lists(stat_type: str, order_type: str = 'DESC', precision: int = 0,
|
||||||
return leaderboard
|
return leaderboard
|
||||||
|
|
||||||
|
|
||||||
def get_variant_ratings():
|
def get_variant_rows() -> List[VariantRow]:
|
||||||
cur = conn_manager.get_new_cursor()
|
cur = conn_manager.get_new_cursor()
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT"
|
"SELECT"
|
||||||
|
" ratings.id,"
|
||||||
" ratings.name,"
|
" ratings.name,"
|
||||||
" ratings.num_players,"
|
" ratings.num_players,"
|
||||||
" current_rating,"
|
" current_rating,"
|
||||||
" COUNT(games.id) AS games_played,"
|
" COUNT(games.id) AS games_played,"
|
||||||
" COUNT(games.id) FILTER (WHERE ratings.num_suits * 5 = games.score) AS games_won "
|
" COUNT(games.id) FILTER (WHERE ratings.num_suits * 5 = games.score) AS games_won,"
|
||||||
|
" SUM(game_statistics.num_bottom_deck_risks) AS total_bdr,"
|
||||||
|
" SUM(game_statistics.num_crits_lost) AS total_crits_lost,"
|
||||||
|
" SUM(games.num_turns) AS total_turns "
|
||||||
"FROM "
|
"FROM "
|
||||||
" ("
|
" ("
|
||||||
" SELECT DISTINCT ON (variants.id, variant_base_ratings.num_players)"
|
" SELECT DISTINCT ON (variants.id, variant_base_ratings.num_players)"
|
||||||
|
@ -141,19 +177,25 @@ def get_variant_ratings():
|
||||||
" LEFT OUTER JOIN variant_base_ratings"
|
" LEFT OUTER JOIN variant_base_ratings"
|
||||||
" ON variants.id = variant_base_ratings.variant_id "
|
" ON variants.id = variant_base_ratings.variant_id "
|
||||||
" LEFT OUTER JOIN variant_ratings "
|
" LEFT OUTER JOIN variant_ratings "
|
||||||
" ON variant_ratings.variant_id = variant_base_ratings.variant_id AND variant_ratings.num_players = variant_base_ratings.num_players "
|
" ON variant_ratings.variant_id = variant_base_ratings.variant_id"
|
||||||
" GROUP BY (variants.id, name, variant_base_ratings.num_players, variant_base_ratings.rating, variant_ratings.league_id, variant_ratings.value_after) "
|
" AND variant_ratings.num_players = variant_base_ratings.num_players "
|
||||||
|
" GROUP BY ("
|
||||||
|
" variants.id, name, variant_base_ratings.num_players, variant_base_ratings.rating,"
|
||||||
|
" variant_ratings.league_id, variant_ratings.value_after"
|
||||||
|
" ) "
|
||||||
" ORDER BY variants.id, variant_base_ratings.num_players, league_id DESC"
|
" ORDER BY variants.id, variant_base_ratings.num_players, league_id DESC"
|
||||||
" ) AS ratings "
|
" ) AS ratings "
|
||||||
"LEFT OUTER JOIN games "
|
"LEFT OUTER JOIN games "
|
||||||
" ON games.variant_id = ratings.id AND games.num_players = ratings.num_players "
|
" ON games.variant_id = ratings.id AND games.num_players = ratings.num_players "
|
||||||
|
"LEFT OUTER JOIN game_statistics"
|
||||||
|
" ON games.id = game_statistics.game_id "
|
||||||
"GROUP BY (ratings.id, ratings.name, ratings.num_players, ratings.current_rating)"
|
"GROUP BY (ratings.id, ratings.name, ratings.num_players, ratings.current_rating)"
|
||||||
"ORDER BY (ratings.id, ratings.num_players)"
|
"ORDER BY (ratings.id, ratings.num_players)"
|
||||||
""
|
""
|
||||||
)
|
)
|
||||||
return [
|
return [
|
||||||
VariantStats(variant_name, num_players, rating, games_played, games_won)
|
VariantRow(variant_id, variant_name, num_players, VariantStats(round(rating), games_played, games_won, total_bdr, total_crits_lost, total_turns))
|
||||||
for (variant_name, num_players, rating, games_played, games_won) in cur.fetchall()
|
for (variant_id, variant_name, num_players, rating, games_played, games_won, total_bdr, total_crits_lost, total_turns) in cur.fetchall()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -171,11 +213,27 @@ def get_num_players():
|
||||||
return num_users
|
return num_users
|
||||||
|
|
||||||
|
|
||||||
|
def build_variant_stats_per_variant(variant_rows: List[VariantRow]):
|
||||||
|
variant_stats = {}
|
||||||
|
variant_names = {}
|
||||||
|
for row in variant_rows:
|
||||||
|
if row.variant_id not in variant_stats.keys():
|
||||||
|
variant_stats[row.variant_id] = {}
|
||||||
|
if row.variant_id not in variant_names.keys():
|
||||||
|
variant_names[row.variant_id] = {}
|
||||||
|
variant_stats[row.variant_id][row.num_players] = row.stats
|
||||||
|
variant_names[row.variant_id] = row.name
|
||||||
|
return variant_stats, variant_names
|
||||||
|
|
||||||
|
|
||||||
def render_leaderboard():
|
def render_leaderboard():
|
||||||
rating_lists = get_rating_lists()
|
rating_lists = get_rating_lists()
|
||||||
streak_lists = get_stat_lists("maximum_streak")
|
streak_lists = get_stat_lists("maximum_streak")
|
||||||
leaders = get_leaders(rating_lists, streak_lists)
|
leaders = get_leaders(rating_lists, streak_lists)
|
||||||
|
|
||||||
|
variant_rows: List[VariantRow] = get_variant_rows()
|
||||||
|
variant_stats, variant_names = build_variant_stats_per_variant(variant_rows)
|
||||||
|
|
||||||
leaderboards = {
|
leaderboards = {
|
||||||
'Player Rating': rating_lists,
|
'Player Rating': rating_lists,
|
||||||
'Maximum Streak': streak_lists,
|
'Maximum Streak': streak_lists,
|
||||||
|
@ -185,21 +243,38 @@ def render_leaderboard():
|
||||||
}
|
}
|
||||||
|
|
||||||
env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates'))
|
env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates'))
|
||||||
|
|
||||||
template = env.get_template('content.html')
|
template = env.get_template('content.html')
|
||||||
# rendered_html = template.render(leaders=leaders, leaderboards=leaderboards, variants=variants)
|
|
||||||
rendered_html = template.render(
|
rendered_html = template.render(
|
||||||
leaders=leaders,
|
leaders=leaders,
|
||||||
leaderboards=leaderboards,
|
leaderboards=leaderboards,
|
||||||
total_games_played=get_total_games(),
|
total_games_played=get_total_games(),
|
||||||
total_players=get_num_players(),
|
total_players=get_num_players(),
|
||||||
latest_run=datetime.datetime.now().isoformat(),
|
latest_run=datetime.datetime.now().isoformat(),
|
||||||
variants=get_variant_ratings()
|
variants=variant_rows
|
||||||
# variants=variants,
|
# variants=variants,
|
||||||
)
|
)
|
||||||
|
|
||||||
output_file = Path(constants.WEBSITE_OUTPUT_DIRECTORY) / 'index.html'
|
output_file = Path(constants.WEBSITE_OUTPUT_DIRECTORY) / 'index.html'
|
||||||
output_file.parent.mkdir(exist_ok=True, parents=True)
|
output_file.parent.mkdir(exist_ok=True, parents=True)
|
||||||
with open(output_file, 'w') as f:
|
with open(output_file, 'w') as f:
|
||||||
f.write(rendered_html)
|
f.write(rendered_html)
|
||||||
|
|
||||||
|
variant_template = env.get_template('variant.html')
|
||||||
|
for variant_id, stats in variant_stats.items():
|
||||||
|
rendered_var = variant_template.render(
|
||||||
|
total_games_played=get_total_games(),
|
||||||
|
total_players=get_num_players(),
|
||||||
|
latest_run=datetime.datetime.now().isoformat(),
|
||||||
|
variant_stats=stats,
|
||||||
|
variant_name=variant_names[variant_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
output_file = Path(constants.WEBSITE_OUTPUT_DIRECTORY) / 'variant' / str(variant_id) / 'index.html'
|
||||||
|
output_file.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
|
with open(output_file, 'w') as f:
|
||||||
|
f.write(rendered_var)
|
||||||
|
|
||||||
|
|
||||||
render_leaderboard()
|
render_leaderboard()
|
||||||
|
|
|
@ -69,9 +69,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td class="text-center"><strong>{{ variant.name }}</strong></td>
|
<td class="text-center"><strong>{{ variant.name }}</strong></td>
|
||||||
<td class="text-center">{{ variant.num_players }}</td>
|
<td class="text-center">{{ variant.num_players }}</td>
|
||||||
<td class="text-center variant-rating">{{ variant.rating | int }}</td>
|
<td class="text-center variant-rating">{{ variant.stats.rating | int }}</td>
|
||||||
<td class="text-center">{{ variant.games_played }}</td>
|
<td class="text-center">{{ variant.stats.games_played }}</td>
|
||||||
<td class="text-center">{{ variant.games_won }}</td>
|
<td class="text-center">{{ variant.stats.games_won }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
131
templates/variant.html
Normal file
131
templates/variant.html
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Hanabi Pro Hunting League</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="../../css/leaderboards.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||||
|
<div class="container">
|
||||||
|
<a class="navbar-brand" href="#">The Hanabi Pro Hunting League<small class="text-muted"> - Variant Statistics for {{variant_name}}</small></a>
|
||||||
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" id="overview-tab" data-toggle="tab" href="#overview">Overview</a>
|
||||||
|
</li>
|
||||||
|
{% for num_players in variant_stats.keys() %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" id="stats-{{num_players}}p-tab" data-toggle="tab" href="#stats-{{num_players}}p">{{num_players}} Players</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="tab-content" id="myTabContent">
|
||||||
|
|
||||||
|
{% for num_players, stats in variant_stats.items() %}
|
||||||
|
<div class="tab-pane fade" id="stats-{{num_players}}p">
|
||||||
|
<div class="container my-5">
|
||||||
|
<h3>
|
||||||
|
League statistics for {{variant_name}} ({{num_players}} players)
|
||||||
|
</h3>
|
||||||
|
<div class="history-bullets">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="stat-description">Rating:</span>
|
||||||
|
{{stats.rating}}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="stat-description">Total perfect scores:</span>
|
||||||
|
{{stats.games_won}}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="stat-description">Total bottom deck risk:</span>
|
||||||
|
{{stats.total_bdr}}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="stat-description">Total crits lost:</span>
|
||||||
|
{{stats.total_crits_lost}}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="stat-description">Total turns taken:</span>
|
||||||
|
{{stats.total_moves}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<span class="stat-description">Total games played:</span>
|
||||||
|
{{stats.games_played}}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="stat-description">Winrate:</span>
|
||||||
|
{{stats.winrate}}%
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="stat-description">Average bottom deck risk:</span>
|
||||||
|
{{stats.average_bdr}}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="stat-description">Average crits lost:</span>
|
||||||
|
{{stats.average_crits_lost}}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="stat-description">Average turns taken:</span>
|
||||||
|
{{stats.average_moves}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="footer mt-auto py-3 bg-light">
|
||||||
|
<div class="container text-center">
|
||||||
|
<span class="text-muted">{{ total_games_played }} games | {{ total_players }} players | Thanks for playing <3</span><br>
|
||||||
|
<span class="text-muted">Last updated: <span id="latestRun">{{ latest_run }}</span></span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Bootstrap JavaScript dependencies -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.7.0.slim.min.js" integrity="sha256-tG5mcZUtJsZvyKAxYLVXrmjKBVLd6VpVccqz/r4ypFE=" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('.nav-link').on('shown.bs.tab', function(e) {
|
||||||
|
// Remove 'active' class from all nav-items
|
||||||
|
$('.nav-link').removeClass('active');
|
||||||
|
|
||||||
|
// Add 'active' class to the current nav-item
|
||||||
|
$(this).addClass('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Display time of latest run in local time format -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
// Get the latest run date in UTC
|
||||||
|
var latestRunUtc = new Date(document.getElementById("latestRun").textContent);
|
||||||
|
|
||||||
|
// Convert it to the local timezone and format it
|
||||||
|
var latestRunLocal = latestRunUtc.toLocaleString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit" });
|
||||||
|
|
||||||
|
// Set the text content of the latestRun span to the local date
|
||||||
|
document.getElementById("latestRun").textContent = latestRunLocal;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in a new issue