User
Deploy theme persistence and navigation fix
e184579
Raw
History Blame Contribute Delete
18.9 kB
@inherits LayoutComponentBase
@implements IDisposable
@using System.Security.Claims
@inject TaskTrackingSystem.WebApp.Localization.UiLanguageService Language
<div class="app-shell flex min-h-screen bg-slate-50 text-slate-950 dark:bg-slate-950 dark:text-slate-50">
<button type="button"
class="sidebar-scrim"
onclick="document.body.classList.remove('tts-sidebar-open');"
aria-label="@AppLocalization.Text("common.closeNavigation", "Close navigation")"></button>
<aside class="app-sidebar sticky top-0 z-40 flex h-screen w-64 shrink-0 flex-col overflow-hidden border-r border-slate-800 bg-slate-950 text-white">
<div class="app-sidebar-brand flex min-h-20 items-center gap-3 px-5 py-4">
<span class="app-sidebar-logo inline-flex h-10 w-10 items-center justify-center rounded-lg text-white shadow-sm">
<i data-lucide="clipboard-check" class="h-5 w-5"></i>
</span>
<span class="app-sidebar-title flex flex-col leading-tight">
<strong class="text-lg font-bold tracking-normal text-white">Taskify</strong>
<span class="mt-0.5 text-[11px] font-semibold uppercase tracking-wide text-slate-400">@AppLocalization.Text("common.workspace", "Workspace")</span>
</span>
<button type="button"
class="control-button mobile-sidebar-close ml-auto hidden h-9 w-9 items-center justify-center rounded-lg border border-white/10 bg-white/10 text-white hover:bg-white/15 hover:text-white lg:hidden"
onclick="document.body.classList.remove('tts-sidebar-open');"
aria-label="@AppLocalization.Text("common.closeNavigation", "Close navigation")">
<i data-lucide="x" class="h-4 w-4"></i>
</button>
</div>
<div class="flex-1 overflow-y-auto pb-28">
<NavMenu @rendermode="new InteractiveServerRenderMode(prerender: false)" />
</div>
<div class="app-sidebar-user absolute inset-x-0 bottom-0 z-20 shrink-0 border-t border-white/10 bg-slate-950/95 p-3 backdrop-blur">
<AuthorizeView>
<Authorized>
<div class="flex items-center rounded-lg border border-white/10 bg-white/[0.04] p-2 transition-colors hover:bg-white/[0.08]">
<span class="flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white font-semibold text-sm shrink-0">
@(context.User.Identity?.Name?.Length > 0 ? context.User.Identity.Name[0].ToString().ToUpper() : "U")
</span>
<div class="ml-3 flex min-w-0 flex-col overflow-hidden">
<span class="max-w-[130px] truncate text-sm font-semibold text-white">@context.User.Identity?.Name</span>
<form id="logout-form" method="post" action="/account/logout" class="mt-0.5">
<AntiforgeryToken />
<button type="button" onclick="document.body.classList.remove('tts-sidebar-open'); document.getElementById('logout-confirm-dialog').style.display='flex';" class="text-xs hover:opacity-90 font-semibold text-left flex items-center gap-1 transition-all text-slate-300">
<i data-lucide="log-out" class="h-3 w-3"></i>
@Language.Texts.SignOut
</button>
</form>
</div>
</div>
</Authorized>
<NotAuthorized>
<a href="/login" class="flex items-center justify-center rounded-lg bg-white/10 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-white/15">
@Language.Texts.SignIn
</a>
</NotAuthorized>
</AuthorizeView>
</div>
</aside>
<main class="app-main flex min-w-0 flex-1 flex-col lg:ml-4">
<header class="app-topbar flex min-h-16 items-center justify-between gap-3 border-b border-slate-200 bg-slate-50/95 px-4 text-slate-900 backdrop-blur dark:border-slate-800 dark:bg-slate-950/90 dark:text-slate-100">
<div class="flex min-w-0 items-center gap-3">
<button type="button"
class="control-button mobile-sidebar-toggle hidden h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-slate-200 bg-white text-slate-600 shadow-sm hover:bg-slate-50 hover:text-teal-700 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-white"
onclick="document.body.classList.add('tts-sidebar-open');"
aria-label="@AppLocalization.Text("common.openNavigation", "Open navigation")">
<i data-lucide="menu" class="h-4 w-4"></i>
</button>
<div class="app-topbar-title min-w-0">
<h1 class="truncate text-base font-bold tracking-normal text-slate-950 dark:text-slate-50">@CurrentPageTitle</h1>
<p class="hidden truncate text-xs font-medium text-slate-500 dark:text-slate-400 sm:block">@CurrentPageDescription</p>
</div>
</div>
<div class="flex shrink-0 items-center gap-2 sm:gap-3">
<AuthorizeView>
<Authorized>
<FirebaseTokenSync />
</Authorized>
</AuthorizeView>
<NotificationBell />
<LanguageToggle />
<ThemeToggle />
</div>
</header>
<div class="app-content flex-1 overflow-y-auto p-5 lg:p-6">
@if (isAuthorizedPage)
{
@Body
}
else
{
<div class="flex flex-col items-center justify-center py-20 px-4">
<div class="rounded-full p-4 mb-4" style="color: var(--brand-accent); background-color: var(--status-warning-bg);">
<i data-lucide="shield-alert" class="h-12 w-12"></i>
</div>
<h3 class="text-xl font-bold" style="color: var(--text-primary);">@Language.Texts.AccessDenied</h3>
<p class="text-center mt-2 max-w-sm text-sm" style="color: var(--text-muted);">
@Language.Texts.AccessDeniedDescription
</p>
<a href="@dashboardHref" class="mt-6 inline-flex items-center rounded-lg px-4 py-2.5 text-sm font-medium text-white transition-colors shadow-sm hover:opacity-90" style="background-color: var(--brand-primary);">
<i data-lucide="arrow-left" class="h-4 w-4 mr-2"></i>
@Language.Texts.BackToDashboard
</a>
</div>
}
</div>
</main>
<div id="logout-confirm-dialog" class="fixed inset-0 z-[80] flex items-center justify-center bg-slate-950/45 px-4 backdrop-blur-sm" style="display: none;">
<div class="w-full max-w-sm overflow-hidden rounded-lg border border-slate-100 bg-white shadow-lg dark:border-slate-800 dark:bg-slate-900">
<div class="p-6 text-center">
<div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full" style="background-color: var(--status-warning-bg); color: var(--status-warning-text);">
<i data-lucide="log-out" class="h-7 w-7"></i>
</div>
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-50">@Language.Texts.SignOutTitle</h3>
<p class="mt-2 text-sm text-slate-500 dark:text-slate-400">@Language.Texts.SignOutDescription</p>
</div>
<div class="flex items-center justify-center space-x-3 border-t border-slate-100 bg-slate-50/50 px-6 py-4 dark:border-slate-800 dark:bg-slate-950/40">
<button type="button" onclick="document.getElementById('logout-confirm-dialog').style.display='none';" class="flex-1 rounded-lg border border-slate-200 bg-white px-4 py-2.5 text-sm font-medium text-slate-500 transition-colors hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800">
@Language.Texts.Cancel
</button>
<button type="button" onclick="document.getElementById('logout-form').submit();" class="flex-1 rounded-lg px-4 py-2.5 text-sm font-medium text-white transition-colors" style="background-color: var(--brand-accent);">
@Language.Texts.ConfirmSignOut
</button>
</div>
</div>
</div>
</div>
@code {
[Inject] private NavigationManager Navigation { get; set; } = default!;
[Inject] private IJSRuntime JS { get; set; } = default!;
[Inject] private MenuAuthorizationService MenuAuthorization { get; set; } = default!;
[Inject] private Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider AuthStateProvider { get; set; } = default!;
private bool isAuthorizedPage = true;
private string dashboardHref = "/dashboard";
private bool IsDashboardPage =>
string.IsNullOrEmpty(Navigation.ToBaseRelativePath(Navigation.Uri)) ||
Navigation.ToBaseRelativePath(Navigation.Uri).Split('?')[0].Trim('/') is "dashboard" or "Home" or "";
private string CurrentRoute => Navigation.ToBaseRelativePath(Navigation.Uri).Split('?')[0].Trim('/');
private string CurrentPageTitle
{
get
{
if (CurrentRoute.StartsWith("projects/", StringComparison.OrdinalIgnoreCase) &&
CurrentRoute.EndsWith("/tasks", StringComparison.OrdinalIgnoreCase))
{
return AppLocalization.PageTitle("kanbanBoard", "Kanban Board");
}
if (CurrentRoute.StartsWith("tasks/", StringComparison.OrdinalIgnoreCase))
{
return AppLocalization.PageTitle("taskDetails", "Task Details");
}
return CurrentRoute switch
{
"" or "dashboard" or "Home" => AppLocalization.PageTitle("dashboard", "Dashboard"),
"dashboard/manager" => AppLocalization.PageTitle("managerDashboard", "Manager Dashboard"),
"dashboard/employee" => AppLocalization.PageTitle("employeeDashboard", "Employee Dashboard"),
"projects" or "projects/all" => AppLocalization.PageTitle("projects", "Projects"),
"projects/assign" => AppLocalization.PageTitle("projectAssignment", "Project Assignment"),
"tasks" or "tasks/all" => AppLocalization.PageTitle("tasks", "Tasks"),
"board" => AppLocalization.PageTitle("kanbanBoard", "Kanban Board"),
"tasks/assign" => AppLocalization.PageTitle("taskAssignment", "Task Assignment"),
"issues" or "issues/all" => AppLocalization.PageTitle("issues", "Issues"),
"issues/add" => AppLocalization.PageTitle("addIssue", "Add Issue"),
"reports/tasks" => AppLocalization.PageTitle("taskReport", "Task Report"),
"reports/time" => AppLocalization.PageTitle("timeTracking", "Time Tracking"),
"reports/projects" => AppLocalization.PageTitle("projectProgress", "Project Progress"),
"reports/overdue" => AppLocalization.PageTitle("overdueTasks", "Overdue Tasks"),
"reports/employees" => AppLocalization.PageTitle("employeeReport", "Employee Report"),
"roles" => AppLocalization.PageTitle("rolesAccess", "Roles & Access"),
"users" => AppLocalization.PageTitle("users", "Users"),
"audit-logs" => AppLocalization.PageTitle("auditLogs", "Audit Logs"),
_ => AppLocalization.Text("common.workspace", "Workspace")
};
}
}
private string CurrentPageDescription => CurrentPageTitle switch
{
var title when title == AppLocalization.PageTitle("dashboard", "Dashboard") => AppLocalization.PageDescription("dashboard", "A quick overview of work, risk, and progress."),
var title when title == AppLocalization.PageTitle("managerDashboard", "Manager Dashboard") => AppLocalization.PageDescription("managerDashboard", "Track projects, people, and delivery health."),
var title when title == AppLocalization.PageTitle("employeeDashboard", "Employee Dashboard") => AppLocalization.PageDescription("employeeDashboard", "Focus on your tasks, due dates, and progress."),
var title when title == AppLocalization.PageTitle("projects", "Projects") => AppLocalization.PageDescription("projects", "Manage project plans, timelines, and assignments."),
var title when title == AppLocalization.PageTitle("projectAssignment", "Project Assignment") => AppLocalization.PageDescription("projectAssignment", "Assign team members to the right projects."),
var title when title == AppLocalization.PageTitle("tasks", "Tasks") => AppLocalization.PageDescription("tasks", "Review, filter, and update work items."),
var title when title == AppLocalization.PageTitle("kanbanBoard", "Kanban Board") => AppLocalization.PageDescription("kanbanBoard", "Move work through status columns."),
var title when title == AppLocalization.PageTitle("taskAssignment", "Task Assignment") => AppLocalization.PageDescription("taskAssignment", "Assign tasks to project members."),
var title when title == AppLocalization.PageTitle("issues", "Issues") => AppLocalization.PageDescription("issues", "Track blockers, notes, and daily work progress."),
var title when title == AppLocalization.PageTitle("addIssue", "Add Issue") => AppLocalization.PageDescription("addIssue", "Record a new issue against a task."),
var title when title == AppLocalization.PageTitle("rolesAccess", "Roles & Access") => AppLocalization.PageDescription("rolesAccess", "Control user roles, menus, and permissions."),
var title when title == AppLocalization.PageTitle("users", "Users") => AppLocalization.PageDescription("users", "Manage people, status, and account details."),
var title when title == AppLocalization.PageTitle("auditLogs", "Audit Logs") => AppLocalization.PageDescription("auditLogs", "Review system activity and changes."),
var title when title.Contains(AppLocalization.Text("page.taskReport", "Task Report"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.taskReport.desc", "Analyze work data and export reports."),
var title when title.Contains(AppLocalization.Text("page.timeTracking", "Time Tracking"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.timeTracking.desc", "Review effort by task, project, and assignee."),
var title when title.Contains(AppLocalization.Text("page.projectProgress", "Project Progress"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.projectProgress.desc", "Monitor delivery health across active projects."),
var title when title.Contains(AppLocalization.Text("page.overdueTasks", "Overdue Tasks"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.overdueTasks.desc", "Focus on delayed work that needs attention."),
var title when title.Contains(AppLocalization.Text("page.employeeReport", "Employee Report"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.employeeReport.desc", "See how work is distributed across people."),
var title when title.Contains(AppLocalization.Text("page.issueReport", "Issue Report"), StringComparison.OrdinalIgnoreCase) => AppLocalization.Text("page.issueReport.desc", "Track daily execution, backlog health, and issue-level effort."),
_ => AppLocalization.Text("common.workspace", "Workspace")
};
protected override async Task OnInitializedAsync()
{
Language.Changed += OnLanguageChanged;
Navigation.LocationChanged += OnLocationChanged;
var relativePath = Navigation.ToBaseRelativePath(Navigation.Uri);
await UpdateAuthorizationAsync(relativePath);
}
private void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
{
UpdateAuthorization();
}
private void UpdateAuthorization()
{
var relativePath = Navigation.ToBaseRelativePath(Navigation.Uri);
_ = UpdateAuthorizationAsync(relativePath);
}
private void OnLanguageChanged()
{
_ = InvokeAsync(StateHasChanged);
}
private async Task UpdateAuthorizationAsync(string relativePath)
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity?.IsAuthenticated != true)
{
if (!MenuAuthorizationService.IsPublicRoute(relativePath))
{
var returnPath = new Uri(Navigation.Uri).AbsolutePath;
Navigation.NavigateTo($"/login?ReturnUrl={Uri.EscapeDataString(returnPath)}");
}
isAuthorizedPage = MenuAuthorizationService.IsPublicRoute(relativePath);
await InvokeAsync(StateHasChanged);
return;
}
var menus = await MenuAuthorization.GetUserMenusAsync(user);
dashboardHref = ResolveDashboardHref(menus);
var allowed = MenuAuthorization.IsRouteAllowed(user, relativePath);
isAuthorizedPage = allowed;
await InvokeAsync(StateHasChanged);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await JS.InvokeVoidAsync("initIcons");
}
public void Dispose()
{
Navigation.LocationChanged -= OnLocationChanged;
Language.Changed -= OnLanguageChanged;
}
private static string ResolveDashboardHref(IEnumerable<TaskTrackingSystem.Shared.Models.Menu.MenuDto> menus)
{
foreach (var menu in menus)
{
var found = ResolveDashboardHref(menu);
if (!string.IsNullOrWhiteSpace(found))
{
return found;
}
}
return "/dashboard";
}
private static string? ResolveDashboardHref(TaskTrackingSystem.Shared.Models.Menu.MenuDto menu)
{
if (!string.IsNullOrWhiteSpace(menu.MenuUrl) &&
menu.MenuUrl.StartsWith("/dashboard", StringComparison.OrdinalIgnoreCase))
{
return menu.MenuUrl;
}
foreach (var subMenu in menu.SubMenus)
{
var found = ResolveDashboardHref(subMenu);
if (!string.IsNullOrWhiteSpace(found))
{
return found;
}
}
return null;
}
}