Improve home page layout
This commit is contained in:
@@ -31,3 +31,6 @@ docker-compose.yml
|
|||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
/WebApp/logs/*
|
/WebApp/logs/*
|
||||||
/WebApp/DataProtectionKeys/*
|
/WebApp/DataProtectionKeys/*
|
||||||
|
|
||||||
|
# Runtime data directory
|
||||||
|
/WebApp/Data/*
|
||||||
|
|||||||
@@ -1,14 +1,107 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
@using WebApp.Models
|
||||||
@inject IConfiguration Configuration
|
@inject IConfiguration Configuration
|
||||||
|
@inject AppDbContext Context
|
||||||
|
|
||||||
<PageTitle>Home</PageTitle>
|
<PageTitle>@Configuration["ChapterSettings:Name"] - TSA Chapter Organizer</PageTitle>
|
||||||
|
|
||||||
<MudImage Fluid="true" Src="TCO_Title.png" Alt="TSA Chapter Organizer"></MudImage>
|
<MudPaper Elevation="0" Class="mb-6">
|
||||||
<MudText Typo="Typo.h3">@Configuration["ChapterSettings:Name"]</MudText>
|
<div class="d-flex flex-column align-center text-center mb-4">
|
||||||
|
<MudImage Fluid="true" Src="TCO_Title.png" Alt="TSA Chapter Organizer" Class="mb-4" Style="max-width: 600px;" />
|
||||||
|
<MudText Typo="Typo.h3" Class="mb-2">
|
||||||
|
<strong>@Configuration["ChapterSettings:Name"]</strong>
|
||||||
|
</MudText>
|
||||||
|
<MudText Typo="Typo.h6" Color="Color.Secondary">
|
||||||
|
@Configuration["ChapterSettings:CompetitionYear"] Competition Year
|
||||||
|
</MudText>
|
||||||
|
</div>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
<MudLink Href="events">
|
<MudGrid>
|
||||||
<MudCard>
|
<!-- Events Card -->
|
||||||
<MudCardHeader>Events</MudCardHeader>
|
<DashboardCard Icon="@AppIcons.Events"
|
||||||
</MudCard>
|
Title="Events"
|
||||||
</MudLink>
|
Count="@_eventCount"
|
||||||
|
Subtitle="Total Events"
|
||||||
|
Caption="@($"{_individualEventsCount} Individual | {_teamEventsCount} Team")"
|
||||||
|
NavigateUrl="/events" />
|
||||||
|
|
||||||
|
<!-- Students Card -->
|
||||||
|
<DashboardCard Icon="@AppIcons.Student"
|
||||||
|
Title="Students"
|
||||||
|
Count="@_studentCount"
|
||||||
|
Subtitle="Active Students"
|
||||||
|
NavigateUrl="/students">
|
||||||
|
@if (!string.IsNullOrEmpty(_gradeDistribution) && _gradeDistribution != "No students yet")
|
||||||
|
{
|
||||||
|
<MudStack>
|
||||||
|
<MudText Typo="Typo.caption">@((MarkupString)_gradeDistribution)</MudText>
|
||||||
|
</MudStack>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Class="mt-2">No students yet</MudText>
|
||||||
|
}
|
||||||
|
</DashboardCard>
|
||||||
|
|
||||||
|
<!-- Teams Card -->
|
||||||
|
<DashboardCard Icon="@AppIcons.Teams"
|
||||||
|
Title="Teams"
|
||||||
|
Count="@_teamCount"
|
||||||
|
Subtitle="Total Teams"
|
||||||
|
NavigateUrl="/teams" />
|
||||||
|
|
||||||
|
<!-- Meeting Schedule Card -->
|
||||||
|
<DashboardCard Icon="@AppIcons.Scheduler"
|
||||||
|
Title="Schedule"
|
||||||
|
Caption="Optimize meeting times"
|
||||||
|
NavigateUrl="/meeting-schedule" />
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private int _eventCount;
|
||||||
|
private int _individualEventsCount;
|
||||||
|
private int _teamEventsCount;
|
||||||
|
private int _studentCount;
|
||||||
|
private string _gradeDistribution = "";
|
||||||
|
private int _teamCount;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadStatistics()
|
||||||
|
{
|
||||||
|
// Events statistics
|
||||||
|
var events = await Context.Events.ToListAsync();
|
||||||
|
_eventCount = events.Count();
|
||||||
|
_individualEventsCount = events.Count(e => e.EventFormat == EventFormat.Individual);
|
||||||
|
_teamEventsCount = events.Count(e => e.EventFormat == EventFormat.Team);
|
||||||
|
|
||||||
|
// Students statistics
|
||||||
|
_studentCount = await Context.Students.CountAsync();
|
||||||
|
|
||||||
|
// Grade distribution
|
||||||
|
var gradeGroups = await Context.Students
|
||||||
|
.GroupBy(s => s.Grade)
|
||||||
|
.Select(g => new { Grade = g.Key, Count = g.Count() })
|
||||||
|
.OrderBy(g => g.Grade)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (gradeGroups.Any())
|
||||||
|
{
|
||||||
|
_gradeDistribution = string.Join(" | ", gradeGroups.Select(g =>
|
||||||
|
$"<span style=\"white-space: nowrap\">{AppIcons.GetOrdinalSuperscript(g.Grade)}: <strong>{g.Count}</strong></span>"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_gradeDistribution = "No students yet";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teams statistics
|
||||||
|
_teamCount = await Context.Teams.CountAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
@using Microsoft.AspNetCore.Components
|
||||||
|
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudCard Elevation="4" Class="pa-4" Style="cursor: pointer; height: 100%;" @onclick="HandleClick">
|
||||||
|
<MudCardContent>
|
||||||
|
<div class="d-flex align-center mb-2">
|
||||||
|
<MudIcon Icon="@Icon" Size="Size.Large" Class="mr-2" />
|
||||||
|
<MudText Typo="Typo.h5">@Title</MudText>
|
||||||
|
</div>
|
||||||
|
<MudDivider Class="mb-3" />
|
||||||
|
@if (Count != null)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.h3" Color="Color.Info">@Count</MudText>
|
||||||
|
<MudText Typo="Typo.body2">@Subtitle</MudText>
|
||||||
|
}
|
||||||
|
@if (ChildContent != null)
|
||||||
|
{
|
||||||
|
@ChildContent
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(Caption))
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Class="mt-2">@Caption</MudText>
|
||||||
|
}
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudItem>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string Icon { get; set; } = string.Empty;
|
||||||
|
[Parameter] public string Title { get; set; } = string.Empty;
|
||||||
|
[Parameter] public int? Count { get; set; }
|
||||||
|
[Parameter] public string? Subtitle { get; set; } = string.Empty;
|
||||||
|
[Parameter] public string? Caption { get; set; }
|
||||||
|
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||||
|
[Parameter] public string NavigateUrl { get; set; } = string.Empty;
|
||||||
|
[Inject] private NavigationManager Navigation { get; set; } = default!;
|
||||||
|
|
||||||
|
private void HandleClick()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(NavigateUrl))
|
||||||
|
{
|
||||||
|
Navigation.NavigateTo(NavigateUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ namespace WebApp.Models
|
|||||||
public static string Student = Icons.Material.Filled.Person;
|
public static string Student = Icons.Material.Filled.Person;
|
||||||
public static string TeamAssignment = Icons.Material.Filled.GroupAdd;
|
public static string TeamAssignment = Icons.Material.Filled.GroupAdd;
|
||||||
public static string Events = Icons.Material.Filled.Dashboard;
|
public static string Events = Icons.Material.Filled.Dashboard;
|
||||||
public static string Scheduler = Icons.Material.Filled.CalendarViewDay;
|
public static string Scheduler = Icons.Material.Filled.CalendarMonth;
|
||||||
public static string LevelOfEffortIcon(int? loe)
|
public static string LevelOfEffortIcon(int? loe)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -110,5 +110,21 @@ namespace WebApp.Models
|
|||||||
return num + "th";
|
return num + "th";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetOrdinalSuperscript(int number)
|
||||||
|
{
|
||||||
|
var suffix = number switch
|
||||||
|
{
|
||||||
|
11 or 12 or 13 => "th",
|
||||||
|
_ => (number % 10) switch
|
||||||
|
{
|
||||||
|
1 => "st",
|
||||||
|
2 => "nd",
|
||||||
|
3 => "rd",
|
||||||
|
_ => "th"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return $"{number}<sup>{suffix}</sup>";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-36
@@ -10,48 +10,60 @@ using WebApp.Components;
|
|||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Load optional appsettings from Data directory (overrides defaults)
|
// Load optional appsettings from Data directory in production (overrides defaults)
|
||||||
var dataAppSettingsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "appsettings.json");
|
// In development, use appsettings.Development.json instead
|
||||||
if (!File.Exists(dataAppSettingsPath))
|
if (builder.Environment.IsProduction())
|
||||||
{
|
{
|
||||||
Console.WriteLine($"appsettings.json not found at {dataAppSettingsPath}. Creating with template values...");
|
var dataAppSettingsPath = Path.Combine(builder.Environment.ContentRootPath, "Data", "appsettings.json");
|
||||||
|
if (!File.Exists(dataAppSettingsPath))
|
||||||
var templateSettings = new
|
|
||||||
{
|
{
|
||||||
ChapterSettings = new
|
Console.WriteLine($"appsettings.json not found at {dataAppSettingsPath}. Creating from template...");
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
Name = "Your Chapter Name",
|
// Ensure Data directory exists
|
||||||
ShortName = "YCN",
|
Directory.CreateDirectory(Path.Combine(builder.Environment.ContentRootPath, "Data"));
|
||||||
NationalId = "0000",
|
|
||||||
StateId = "00000",
|
// Copy ChapterSettings from the base appsettings.json
|
||||||
RegionalId = "00000",
|
var baseAppSettingsPath = Path.Combine(builder.Environment.ContentRootPath, "appsettings.json");
|
||||||
CompetitionYear = "2025"
|
if (File.Exists(baseAppSettingsPath))
|
||||||
|
{
|
||||||
|
var baseConfig = File.ReadAllText(baseAppSettingsPath);
|
||||||
|
var baseDoc = JsonDocument.Parse(baseConfig);
|
||||||
|
|
||||||
|
if (baseDoc.RootElement.TryGetProperty("ChapterSettings", out var chapterSettings))
|
||||||
|
{
|
||||||
|
var templateSettings = new
|
||||||
|
{
|
||||||
|
ChapterSettings = JsonSerializer.Deserialize<object>(chapterSettings.GetRawText())
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(templateSettings, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
|
||||||
|
File.WriteAllText(dataAppSettingsPath, json);
|
||||||
|
Console.WriteLine("appsettings.json created in Data directory. Please update ChapterSettings with your chapter information.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("WARNING: ChapterSettings not found in base appsettings.json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"WARNING: Unable to create appsettings.json: {ex.Message}");
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(templateSettings, new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
WriteIndented = true
|
|
||||||
});
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Ensure Data directory exists
|
|
||||||
Directory.CreateDirectory(Path.Combine(builder.Environment.ContentRootPath, "Data"));
|
|
||||||
File.WriteAllText(dataAppSettingsPath, json);
|
|
||||||
Console.WriteLine("appsettings.json created in Data directory. Please update ChapterSettings with your chapter information.");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"WARNING: Unable to create appsettings.json: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add Data/appsettings.json as additional configuration source
|
// Add Data/appsettings.json as additional configuration source
|
||||||
if (File.Exists(dataAppSettingsPath))
|
if (File.Exists(dataAppSettingsPath))
|
||||||
{
|
{
|
||||||
builder.Configuration.AddJsonFile(dataAppSettingsPath, optional: true, reloadOnChange: true);
|
builder.Configuration.AddJsonFile(dataAppSettingsPath, optional: true, reloadOnChange: true);
|
||||||
Console.WriteLine($"Loaded configuration from {dataAppSettingsPath}");
|
Console.WriteLine($"Loaded configuration from {dataAppSettingsPath}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure Serilog
|
// Configure Serilog
|
||||||
|
|||||||
Reference in New Issue
Block a user