Updated for DawnTrail

main
ilitirit 1 year ago
parent 5a32a7a193
commit 7a4904b7ca

@ -1,5 +1,5 @@
# Set the base image # Set the base image
FROM mcr.microsoft.com/dotnet/sdk:7.0.202-jammy-arm64v8 AS build FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy-arm64v8 AS build
# Set the working directory # Set the working directory
WORKDIR /src WORKDIR /src
@ -23,7 +23,7 @@ ARG TARGET_RUNTIME=linux-x64
RUN dotnet publish "Expedience.Web.csproj" -c Release -o /app/publish --no-self-contained --runtime $TARGET_RUNTIME RUN dotnet publish "Expedience.Web.csproj" -c Release -o /app/publish --no-self-contained --runtime $TARGET_RUNTIME
# Set the base image for the final runtime # Set the base image for the final runtime
FROM mcr.microsoft.com/dotnet/aspnet:7.0.4-jammy-arm64v8 FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy-arm64v8
# Set the working directory # Set the working directory
WORKDIR /app WORKDIR /app

@ -19,7 +19,7 @@ namespace Expedience.Web.Pages
public async Task OnGet(string? expac, CancellationToken cancellationToken) public async Task OnGet(string? expac, CancellationToken cancellationToken)
{ {
if (expac == null) expac = "EW"; if (expac == null) expac = "DT";
if (expac == "DeepDungeons") if (expac == "DeepDungeons")
{ {

@ -0,0 +1,301 @@
@page "/{recordType}/{territoryId}"
@model Expedience.Web.Pages.RecordsModel
@{
ViewData["Title"] = "Expedience";
var roleOrder = new Dictionary<string, int>
{
{"Tank", 0},
{"Healer", 1},
{"DPS", 2}
};
var jobRoles = new Dictionary<string, string>
{
{"MRD", "Tank"}, {"GLA", "Tank"}, {"PLD", "Tank"}, {"GNB", "Tank"}, {"DRK", "Tank"}, {"WAR", "Tank"},
{"CNJ", "Healer"}, {"SCH", "Healer"}, {"SGE", "Healer"}, {"WHM", "Healer"}, {"AST", "Healer"}
};
Func<string, string> getRole = job => jobRoles.ContainsKey(job) ? jobRoles[job] : "DPS";
<style>
#topTenResultsTable {
width: 100%;
table-layout: fixed;
}
#topTenResultsTable th,
#topTenResultsTable td {
padding: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#topTenResultsTable .expand-column {
width: 3%;
text-align: center;
}
#topTenResultsTable .rank-column {
width: 7%;
text-align: center;
}
#topTenResultsTable .player-column {
width: 25%;
}
#topTenResultsTable .job-column {
width: 7%;
text-align: center;
}
#topTenResultsTable .date-column {
width: 10%;
}
#topTenResultsTable .world-column {
width: 10%;
}
#topTenResultsTable .duration-column {
width: 10%;
}
#topTenResultsTable .rank-column,
#topTenResultsTable .job-column {
vertical-align: middle;
}
.expand-button {
background: none;
border: none;
color: white;
font-size: 1.2em;
cursor: pointer;
}
.duty-members-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.duty-members-table td {
padding: 0;
text-align: center;
vertical-align: middle;
}
.duty-members-table .member-icon {
width: 32px;
padding: 0;
line-height: 0;
}
.duty-members-table img {
display: block;
width: 32px;
height: 32px;
}
.duty-members-container {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 5px 0;
}
.hidden {
display: none;
}
.duty-details {
font-size: 0.9em;
margin-left: 20px;
}
.duty-details p {
margin: 2px 0;
}
.duty-details strong {
font-weight: bold;
color: #aaa;
}
.duty-info-container {
display: flex;
align-items: flex-start;
padding: 5px 0;
}
.duty-members-container {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 5px 0;
}
.duty-members-group {
display: flex;
flex-wrap: wrap;
margin-bottom: 2px;
}
.member-icon {
width: 32px;
height: 32px;
margin: 1px;
padding: 0;
display: block;
}
.player-icon {
box-shadow: 0 0 5px 3px rgba(255, 215, 0, 0.7);
border-radius: 50%;
}
.duty-details {
font-size: 0.9em;
padding-left: 8px;
}
.detail-row {
display: flex;
margin: 2px 0;
}
.detail-label {
font-weight: bold;
color: #aaa;
width: 100px; /* Adjust this value based on your longest label */
padding-right: 10px;
text-align: left;
}
.detail-value {
flex: 1;
text-align: left;
}
#topTenResultsTable td {
padding: 8px;
}
.duty-header {
font-size: 1.5em;
font-weight: bold;
margin-bottom: 20px;
color: #fff; /* Adjust color as needed */
text-align: center;
}
</style>
var dutyName = Model.Results.FirstOrDefault()?.Territory?.ContentName ?? "Unknown Duty";
<h2 class="duty-header">@dutyName - @Model.Mode</h2>
<table class="table table-dark table-striped" id="topTenResultsTable" data-toggle="table">
<thead>
<tr>
<th class="expand-column"></th>
<th class="rank-column" data-sortable="true">Rank</th>
<th class="player-column" data-sortable="true">Player</th>
<th class="job-column" data-sortable="true">Job</th>
<th class="duration-column" data-sortable="true" data-sorter="durationSorter">Duration</th>
<th class="date-column" data-sortable="true">Date</th>
<th class="world-column" data-sortable="true">Data Center</th>
</tr>
</thead>
<tbody>
@for (int i = 0; i < Math.Min(Model.Results.Count, 10); i++)
{
var result = Model.Results[i];
var playerMember = result.DutyMembers.FirstOrDefault(m => m.IsPlayer == true);
<tr>
<td>
<button class="expand-button" onclick="toggleDutyMembers(this, '@i')">+</button>
</td>
<td class="rank-column">@(i + 1)</td>
<td class="player-column">@result.User.UserName</td>
<td class="job-column">
@if (playerMember != null)
{
<img src="~/images/@(playerMember.ClassJob.ToLower()).png" alt="@playerMember.ClassJob" title="@playerMember.ClassJob" width="24" height="24" />
}
</td>
<td class="duration-column">@($"{result.Hours:D2}:{result.Minutes:D2}:{result.Seconds:D2}.{result.Milliseconds:D3}")</td>
<td class="date-column">@result.EndTime.ToString("yyyy-MM-dd")</td>
<td class="world-column">@result.DataCenter</td>
</tr>
<tr class="hidden" id="dutyMembers-@i">
<td></td>
<td></td>
<td>
<div class="duty-members-container">
@foreach (var group in result.DutyMembers.GroupBy(m => m.GroupNumber).OrderBy(g => g.Key))
{
<div class="duty-members-group">
@foreach (var member in group
.OrderBy(m => roleOrder[getRole(m.ClassJob)])
.ThenBy(m => m.ClassJob))
{
<img class="member-icon @(member.IsPlayer ? "player-icon" : "")"
src="~/images/@(member.ClassJob).png"
alt="@member.ClassJob"
title="@member.ClassJob" />
}
</div>
}
</div>
</td>
<td colspan="4">
<div class="duty-details">
<div class="detail-row">
<div class="detail-label">Start Time:</div>
<div class="detail-value">@result.StartTime.ToString("yyyy-MM-dd HH:mm:ss")</div>
</div>
<div class="detail-row">
<div class="detail-label">End Time:</div>
<div class="detail-value">@result.EndTime.ToString("yyyy-MM-dd HH:mm:ss")</div>
</div>
<div class="detail-row">
<div class="detail-label">Uploaded At:</div>
<div class="detail-value">@result.UploadedAt.ToString("yyyy-MM-dd HH:mm:ss")</div>
</div>
<div class="detail-row">
<div class="detail-label">Game Version:</div>
<div class="detail-value">@result.GameVersion</div>
</div>
<div class="detail-row">
<div class="detail-label">Plugin Version:</div>
<div class="detail-value">@result.PluginVersion</div>
</div>
</div>
</td>
</tr>
}
</tbody>
</table>
<br />
<a href="javascript:history.back()">Go Back</a>
<script>
function toggleDutyMembers(button, index) {
var row = document.getElementById('dutyMembers-' + index);
if (row.style.display === 'none' || row.style.display === '') {
row.style.display = 'table-row';
button.textContent = '-';
} else {
row.style.display = 'none';
button.textContent = '+';
}
}
</script>
}

@ -0,0 +1,45 @@
using System;
using Expedience.Infrastructure.Models;
using Expedience.Web.Services;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Expedience.Web.Pages
{
public class RecordsModel(IApiService apiService) : PageModel
{
public List<DutyCompletionResult> Results { get; set; }
public string Mode { get; set; }
public async Task OnGet(string recordType, int territoryId, CancellationToken cancellationToken)
{
var recordTypeNum = 0;
if (recordType == "normal")
{
Mode = "Normal";
recordTypeNum = 1;
}
else if (recordType == "solo")
{
Mode = "Solo";
recordTypeNum = 2;
}
else if (recordType == "mine")
{
Mode = "MINE";
recordTypeNum = 3;
}
else if (recordType == "npc")
{
Mode = "NPC Assisted";
recordTypeNum = 4;
}
else
throw new Exception($"Record Type {recordType} not found");
Results = await apiService.GetTopXRecords(recordTypeNum, territoryId, 10, cancellationToken);
}
}
}

@ -28,25 +28,25 @@
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between"> <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1"> <ul class="navbar-nav flex-grow-1">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-@(Accessor.HttpContext.Request.Path.Value == "/DT" || Accessor.HttpContext.Request.Path.Value == "/"? "light" : "muted")" asp-area="" asp-route-expac="DT">Dawntrail</a> <a class="nav-link @(Accessor.HttpContext.Request.Path.Value == "/DT" || Accessor.HttpContext.Request.Path.Value == "/" ? "text-light" : "text-muted")" asp-page="/Index" asp-route-expac="DT">Dawntrail</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-@(Accessor.HttpContext.Request.Path.Value == "/EW" ? "light" : "muted")" asp-area="" asp-route-expac="EW">Endwalker</a> <a class="nav-link @(Accessor.HttpContext.Request.Path.Value == "/EW" ? "text-light" : "text-muted")" asp-page="/Index" asp-route-expac="EW">Endwalker</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-@(Accessor.HttpContext.Request.Path.Value == "/ShB" ? "light" : "muted")" asp-area="" asp-route-expac="ShB">Shadowbringers</a> <a class="nav-link text-@(Accessor.HttpContext.Request.Path.Value == "/ShB" ? "light" : "muted")" asp-area="" asp-page="/Index" asp-route-expac="ShB">Shadowbringers</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-@(Accessor.HttpContext.Request.Path.Value == "/StB" ? "light" : "muted")" asp-area="" asp-route-expac="StB">Stormblood</a> <a class="nav-link text-@(Accessor.HttpContext.Request.Path.Value == "/StB" ? "light" : "muted")" asp-area="" asp-page="/Index" asp-route-expac="StB">Stormblood</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-@(Accessor.HttpContext.Request.Path.Value == "/HW" ? "light" : "muted")" asp-area="" asp-route-expac="HW">Heavensward</a> <a class="nav-link text-@(Accessor.HttpContext.Request.Path.Value == "/HW" ? "light" : "muted")" asp-area="" asp-page="/Index" asp-route-expac="HW">Heavensward</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-@(Accessor.HttpContext.Request.Path.Value == "/ARR" ? "light" : "muted")" asp-area="" asp-route-expac="ARR">A Realm Reborn</a> <a class="nav-link text-@(Accessor.HttpContext.Request.Path.Value == "/ARR" ? "light" : "muted")" asp-area="" asp-page="/Index" asp-route-expac="ARR">A Realm Reborn</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-@(Accessor.HttpContext.Request.Path.Value == "/DeepDungeons" ? "light" : "muted")" asp-area="" asp-route-expac="DeepDungeons">Deep Dungeons</a> <a class="nav-link text-@(Accessor.HttpContext.Request.Path.Value == "/DeepDungeons" ? "light" : "muted")" asp-page="/Index" asp-area="" asp-route-expac="DeepDungeons">Deep Dungeons</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -61,7 +61,7 @@
<footer class="border-top footer text-muted"> <footer class="border-top footer text-muted">
<div class="container"> <div class="container">
&copy; 2023 - Expedience - "LMAO ITS JUST PELOTON FOR RAIDS"</a> &copy; 2024 - Expedience - "LMAO ITS JUST PELOTON FOR RAIDS"</a>
</div> </div>
</footer> </footer>

@ -11,7 +11,8 @@
<th></th> <th></th>
<th colspan="2">Normal</th> <th colspan="2">Normal</th>
<th colspan="2">Solo</th> <th colspan="2">Solo</th>
<th colspan="2">Min iLvl No Echo</th> <th colspan="2">MINE</th>
<th colspan="2">NPC</th>
</tr> </tr>
<tr> <tr>
<th data-sortable="true">Level</th> <th data-sortable="true">Level</th>
@ -22,6 +23,8 @@
<th data-sortable="true" data-sorter="durationSorter">Avg</th> <th data-sortable="true" data-sorter="durationSorter">Avg</th>
<th data-sortable="true" data-sorter="durationSorter">Min</th> <th data-sortable="true" data-sorter="durationSorter">Min</th>
<th data-sortable="true" data-sorter="durationSorter">Avg</th> <th data-sortable="true" data-sorter="durationSorter">Avg</th>
<th data-sortable="true" data-sorter="durationSorter">Min</th>
<th data-sortable="true" data-sorter="durationSorter">Avg</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -29,13 +32,15 @@
{ {
<tr> <tr>
<td>@record.Level</td> <td>@record.Level</td>
<td><a href="./@record.TerritoryId">@record.ContentName</a></td> <td>@record.ContentName</td>
<td>@record.MinNormal.Format()</td> <td><a href="./normal/@record.TerritoryId">@record.MinNormal.Format()</a></td>
<td>@record.AvgNormal.Format()</td> <td>@record.AvgNormal.Format()</td>
<td>@record.MinSolo.Format()</td> <td><a href="./solo/@record.TerritoryId">@record.MinSolo.Format()</a></td>
<td>@record.AvgSolo.Format()</td> <td>@record.AvgSolo.Format()</td>
<td>@record.MinMine.Format()</td> <td><a href="./mine/@record.TerritoryId">@record.MinMine.Format()</a></td>
<td>@record.AvgMine.Format()</td> <td>@record.AvgMine.Format()</td>
<td><a href="./npc/@record.TerritoryId">@record.MinNpc.Format()</a></td>
<td>@record.AvgNpc.Format()</td>
</tr> </tr>
} }
</tbody> </tbody>

@ -7,7 +7,7 @@
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
}, },
"dotnetRunMessages": true, "dotnetRunMessages": true,
"applicationUrl": "http://localhost:5108" "applicationUrl": "http://localhost:5120"
}, },
"https": { "https": {
"commandName": "Project", "commandName": "Project",
@ -16,7 +16,7 @@
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
}, },
"dotnetRunMessages": true, "dotnetRunMessages": true,
"applicationUrl": "https://localhost:7079;http://localhost:5108" "applicationUrl": "https://localhost:7079;http://localhost:5120"
}, },
"IIS Express": { "IIS Express": {
"commandName": "IISExpress", "commandName": "IISExpress",

@ -8,6 +8,7 @@ public interface IApiService
{ {
public ValueTask<List<DutyCompletionRecord>?> GetDutyCompletionRecordsAsync(string expac, CancellationToken cancellationToken); public ValueTask<List<DutyCompletionRecord>?> GetDutyCompletionRecordsAsync(string expac, CancellationToken cancellationToken);
public ValueTask<List<DeepDungeonRecord>?> GetDeepDungeonRecordsAsync(CancellationToken cancellationToken); public ValueTask<List<DeepDungeonRecord>?> GetDeepDungeonRecordsAsync(CancellationToken cancellationToken);
public ValueTask<List<DutyCompletionResult>?> GetTopXRecords(int recordType, int territoryId, int limit, CancellationToken cancellationToken);
} }
public class ApiService : IApiService public class ApiService : IApiService
@ -55,4 +56,24 @@ public class ApiService : IApiService
return new List<DeepDungeonRecord>(); return new List<DeepDungeonRecord>();
} }
public async ValueTask<List<DutyCompletionResult>> GetTopXRecords(int recordType, int territoryId, int limit, CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
try
{
var records = await _httpClient.GetFromJsonAsync<List<DutyCompletionResult>?>($"/api/TopX/{recordType}/{territoryId}/{limit}", cancellationToken);
_logger.LogInformation("Retrieved Top {limit} records for {territoryId} {recordType} in {duration}ms",
limit, territoryId, recordType, stopwatch.ElapsedMilliseconds);
return records;
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred trying to retrieve Top {limit} records for {territoryId} {recordType}: {errorMessage}",
limit, territoryId, recordType, ex.Message);
}
return new List<DutyCompletionResult>();
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Loading…
Cancel
Save