Replace bootstrap with tailwind for admin (#113)

This commit is contained in:
Leendert de Borst
2024-07-21 20:38:57 +02:00
parent 6d16ff234a
commit 8cda9c06a2
33 changed files with 3706 additions and 302 deletions

View File

@@ -0,0 +1,20 @@
@page "/user/logout"
@using AliasVault.Admin2.Services
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject NavigationManager NavigationManager
@inject GlobalNotificationService GlobalNotificationService
@code {
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
// Sign out the user
await SignInManager.SignOutAsync();
GlobalNotificationService.ClearMessages();
// Redirect to the home page with hard refresh.
NavigationManager.NavigateTo("/", true);
}
}

View File

@@ -25,59 +25,6 @@
<ProjectReference Include="..\Databases\AliasServerDb\AliasServerDb.csproj" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="Components\Account\Pages\AccessDenied.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\ConfirmEmail.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\ConfirmEmailChange.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\ExternalLogin.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\ForgotPassword.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\ForgotPasswordConfirmation.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\InvalidPasswordReset.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\InvalidUser.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Lockout.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Login.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\LoginWith2fa.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\LoginWithRecoveryCode.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Manage\ChangePassword.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Manage\DeletePersonalData.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Manage\Disable2fa.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Manage\Email.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Manage\EnableAuthenticator.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Manage\ExternalLogins.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Manage\GenerateRecoveryCodes.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Manage\Index.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Manage\PersonalData.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Manage\ResetAuthenticator.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Manage\SetPassword.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Manage\TwoFactorAuthentication.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Manage\_Imports.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\Register.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\RegisterConfirmation.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\ResendEmailConfirmation.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\ResetPassword.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\ResetPasswordConfirmation.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Pages\_Imports.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Shared\AccountLayout.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Shared\ExternalLoginPicker.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Shared\ManageLayout.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Shared\ManageNavMenu.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Shared\RedirectToLogin.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Shared\ShowRecoveryCodes.razor" />
<_ContentIncludedByDefault Remove="Components\Account\Shared\StatusMessage.razor" />
<_ContentIncludedByDefault Remove="Account\Pages\Manage\ChangePassword.razor" />
<_ContentIncludedByDefault Remove="Account\Pages\Manage\DeletePersonalData.razor" />
<_ContentIncludedByDefault Remove="Account\Pages\Manage\Disable2fa.razor" />
<_ContentIncludedByDefault Remove="Account\Pages\Manage\Email.razor" />
<_ContentIncludedByDefault Remove="Account\Pages\Manage\EnableAuthenticator.razor" />
<_ContentIncludedByDefault Remove="Account\Pages\Manage\GenerateRecoveryCodes.razor" />
<_ContentIncludedByDefault Remove="Account\Pages\Manage\Index.razor" />
<_ContentIncludedByDefault Remove="Account\Pages\Manage\PersonalData.razor" />
<_ContentIncludedByDefault Remove="Account\Pages\Manage\ResetAuthenticator.razor" />
<_ContentIncludedByDefault Remove="Account\Pages\Manage\SetPassword.razor" />
<_ContentIncludedByDefault Remove="Account\Pages\Manage\TwoFactorAuthentication.razor" />
<_ContentIncludedByDefault Remove="Account\Pages\Manage\_Imports.razor" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Account\Pages\AccessDenied.razor" />
<AdditionalFiles Include="Account\Pages\ConfirmEmail.razor" />
@@ -98,9 +45,6 @@
<AdditionalFiles Include="Account\Pages\ResetPasswordConfirmation.razor" />
<AdditionalFiles Include="Account\Pages\_Imports.razor" />
<AdditionalFiles Include="Account\Shared\AuthLayout.razor" />
<AdditionalFiles Include="Account\Shared\ExternalLoginPicker.razor" />
<AdditionalFiles Include="Account\Shared\ManageLayout.razor" />
<AdditionalFiles Include="Account\Shared\ManageNavMenu.razor" />
<AdditionalFiles Include="Account\Shared\RedirectToLogin.razor" />
<AdditionalFiles Include="Account\Shared\ShowRecoveryCodes.razor" />
<AdditionalFiles Include="Account\Shared\StatusMessage.razor" />

View File

@@ -5,15 +5,54 @@
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<base href="/"/>
<link rel="stylesheet" href="bootstrap/bootstrap.min.css"/>
<link rel="stylesheet" href="app.css"/>
<link rel="stylesheet" href="css/tailwind.css"/>
<link rel="stylesheet" href="css/app.css"/>
<link rel="stylesheet" href="AliasVault.Admin2.styles.css"/>
<link rel="icon" type="image/png" href="favicon.png"/>
<HeadOutlet @rendermode="RenderModeForPage"/>
</head>
<body>
<body class="bg-gray-50 dark:bg-gray-800">
<Routes @rendermode="RenderModeForPage"/>
<script src="js/dark-mode.js?v=dev"></script>
<script src="js/utilities.js?v=dev"></script>
<script>
window.initTopMenu = function() {
initDarkModeSwitcher();
};
window.registerClickOutsideHandler = (dotNetHelper) => {
document.addEventListener('click', (event) => {
const menu = document.getElementById('userMenuDropdown');
const menuButton = document.getElementById('userMenuDropdownButton');
if (menu && !menu.contains(event.target) && !menuButton.contains(event.target)) {
dotNetHelper.invokeMethodAsync('CloseMenu');
}
const mobileMenu = document.getElementById('mobileMenu');
const mobileMenuButton = document.getElementById('toggleMobileMenuButton');
if (mobileMenu && !mobileMenu.contains(event.target) && !mobileMenuButton.contains(event.target)) {
dotNetHelper.invokeMethodAsync('CloseMenu');
}
});
};
window.clipboardCopy = {
copyText: function (text) {
navigator.clipboard.writeText(text).then(function () { })
.catch(function (error) {
alert(error);
});
}
};
// Primarily used by E2E tests.
window.blazorNavigate = (url) => {
Blazor.navigateTo(url);
};
</script>
<script src="_framework/blazor.web.js"></script>
</body>

View File

@@ -0,0 +1,10 @@
<footer class="md:flex md:items-center md:justify-between px-4 2xl:px-0 py-6 md:py-10">
<p class="text-sm text-center text-gray-500 mb-4 md:mb-0">
© 2024 AliasVault. All rights reserved.
</p>
<ul class="flex flex-wrap items-center justify-center">
<li><a href="#" class="mr-4 text-sm font-normal text-gray-500 hover:underline md:mr-6 dark:text-gray-400">Terms and conditions</a></li>
<li><a href="#" class="mr-4 text-sm font-normal text-gray-500 hover:underline md:mr-6 dark:text-gray-400">License</a></li>
<li><a href="https://github.com/lanedirt/AliasVault" target="_blank" class="text-sm font-normal text-gray-500 hover:underline dark:text-gray-400">GitHub</a></li>
</ul>
</footer>

View File

@@ -0,0 +1,83 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -2,37 +2,14 @@
@implements IDisposable
@inject NavigationManager NavigationManager
<div class="page">
<div class="sidebar">
<NavMenu/>
</div>
<main>
<div class="top-row px-4">
<AuthorizeView>
<Authorized>
<div class="nav-item px-3">
<NavLink class="nav-link" href="account/manage">
<span class="bi bi-person-fill-nav-menu" aria-hidden="true"></span> @context.User.Identity?.Name
</NavLink>
</div>
<div class="nav-item px-3">
<form action="Account/Logout" method="post">
<AntiforgeryToken/>
<input type="hidden" name="ReturnUrl" value="@currentUrl"/>
<button type="submit" class="nav-link">
<span class="bi bi-arrow-bar-left-nav-menu" aria-hidden="true"></span> Logout
</button>
</form>
</div>
</Authorized>
</AuthorizeView>
</div>
<article class="content px-4">
<TopMenu />
<div class="flex pt-16 overflow-hidden bg-gray-50 dark:bg-gray-900">
<div id="main-content" class="relative w-full max-w-screen-2xl mx-auto h-full overflow-y-auto bg-gray-50 dark:bg-gray-900">
<main>
@Body
</article>
</main>
</main>
<Footer />
</div>
</div>
<div id="blazor-error-ui">

View File

@@ -1,23 +0,0 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">AliasVault Admin</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler"/>
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="emails" Match="NavLinkMatch.Prefix">
<span class="bi bi-lock-nav-menu" aria-hidden="true"></span> Emails
</NavLink>
</div>
</nav>
</div>

View File

@@ -1,125 +0,0 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.bi-lock-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2zM5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z'/%3E%3C/svg%3E");
}
.bi-person-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person' viewBox='0 0 16 16'%3E%3Cpath d='M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z'/%3E%3C/svg%3E");
}
.bi-person-badge-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-badge' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z'/%3E%3Cpath d='M4.5 0A2.5 2.5 0 0 0 2 2.5V14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2.5A2.5 2.5 0 0 0 11.5 0h-7zM3 2.5A1.5 1.5 0 0 1 4.5 1h7A1.5 1.5 0 0 1 13 2.5v10.795a4.2 4.2 0 0 0-.776-.492C11.392 12.387 10.063 12 8 12s-3.392.387-4.224.803a4.2 4.2 0 0 0-.776.492V2.5z'/%3E%3C/svg%3E");
}
.bi-person-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-fill' viewBox='0 0 16 16'%3E%3Cpath d='M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z'/%3E%3C/svg%3E");
}
.bi-arrow-bar-left-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-arrow-bar-left' viewBox='0 0 16 16'%3E%3Cpath d='M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5ZM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5Z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -0,0 +1,142 @@
@inherits MainBase
@implements IDisposable
<header>
<nav class="fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700 py-3 px-4 bg-primary-100">
<div class="flex justify-between items-center max-w-screen-2xl mx-auto">
<div class="flex justify-start items-center">
<a href="/" class="flex mr-14">
<img src="/icon-trimmed.png" class="mr-3 h-8" alt="AliasVault Logo">
<span class="self-center hidden sm:flex text-2xl font-semibold whitespace-nowrap dark:text-white">AliasVault</span>
<span class="ps-2 self-center hidden sm:flex text-2xl font-black whitespace-nowrap dark:text-white">Admin</span>
</a>
<div class="hidden justify-between items-center w-full lg:flex lg:w-auto lg:order-1">
<ul class="flex flex-col mt-4 space-x-6 text-sm font-medium lg:flex-row xl:space-x-8 lg:mt-0">
<NavLink href="/emails" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Emails
</NavLink>
</ul>
</div>
</div>
<div class="flex justify-end items-center lg:order-2">
<button id="theme-toggle" data-tooltip-target="tooltip-toggle" type="button" class="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5">
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
<svg id="theme-toggle-light-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
</button>
<div id="tooltip-toggle" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip" data-popper-placement="bottom" style="position: absolute; inset: 0px auto auto 0px; margin: 0px; transform: translate3d(1377px, 60px, 0px);">
Toggle dark mode
<div class="tooltip-arrow" data-popper-arrow="" style="position: absolute; left: 0px; transform: translate3d(68.5px, 0px, 0px);"></div>
</div>
<button @onclick="ToggleMenu" type="button" class="flex mx-3 text-sm bg-gray-800 rounded-full md:mr-0 flex-shrink-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600" id="userMenuDropdownButton" aria-expanded="false" data-dropdown-toggle="userMenuDropdown">
<span class="sr-only">Open user menu</span>
<img class="w-8 h-8 rounded-full" src="/img/avatar.webp" alt="user photo">
</button>
@if (isMenuOpen)
{
<div class="absolute top-10 right-0 z-50 my-4 w-56 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600" id="userMenuDropdown" data-popper-placement="bottom">
<div class="py-3 px-4">
<span class="block text-sm font-semibold text-gray-900 dark:text-white">@_username</span>
</div>
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="userMenuDropdownButton">
<li>
<a href="account/manage" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">Account settings</a>
</li>
</ul>
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="dropdown">
<li>
<a href="/user/logout" class="block py-2 px-4 font-bold text-sm text-primary-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-primary-200 dark:hover:text-white">Sign out</a>
</li>
</ul>
</div>
}
<button @onclick="ToggleMobileMenu" type="button" id="toggleMobileMenuButton" class="items-center p-2 text-gray-500 rounded-lg md:ml-2 lg:hidden hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-700 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600">
<span class="sr-only">Open menu</span>
<svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>
</button>
</div>
</div>
</nav>
@if (isMobileMenuOpen)
{
<nav class="bg-white dark:bg-gray-900">
<ul id="mobileMenu" class="flex-col mt-0 pt-16 w-full text-sm font-medium lg:hidden">
<li class="block border-b dark:border-gray-700">
<NavLink href="/" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Home
</NavLink>
</li>
<li class="block border-b dark:border-gray-700">
<NavLink href="/credentials" class="block py-3 px-4 text-gray-900 lg:py-0 dark:text-white lg:hover:underline lg:px-0" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
Credentials
</NavLink>
</li>
</ul>
</nav>
}
</header>
@code {
private bool isMenuOpen = false;
private bool isMobileMenuOpen = false;
private string _username { get; set; } = "";
/// <summary>
/// Close the menu.
/// </summary>
[JSInvokable]
public void CloseMenu()
{
isMenuOpen = false;
isMobileMenuOpen = false;
StateHasChanged();
}
/// <summary>
/// Dispose method.
/// </summary>
public void Dispose()
{
NavigationManager.LocationChanged -= LocationChanged;
}
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
_username = GetUsername();
NavigationManager.LocationChanged += LocationChanged;
}
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await Js.InvokeVoidAsync("window.initTopMenu");
DotNetObjectReference<TopMenu> objRef = DotNetObjectReference.Create(this);
await Js.InvokeVoidAsync("window.registerClickOutsideHandler", objRef);
}
}
private void LocationChanged(object? sender, LocationChangedEventArgs e)
{
isMenuOpen = false;
isMobileMenuOpen = false;
StateHasChanged();
}
private void ToggleMenu()
{
isMenuOpen = !isMenuOpen;
}
private void ToggleMobileMenu()
{
isMobileMenuOpen = !isMobileMenuOpen;
}
}

View File

@@ -0,0 +1,83 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -11,23 +11,22 @@
<PageTitle>Profile</PageTitle>
<h3>Profile</h3>
<StatusMessage/>
<div class="row">
<div class="col-md-6">
<div class="flex flex-wrap -mx-3">
<div class="w-full md:w-1/2 px-3">
<EditForm Model="Input" FormName="profile" OnValidSubmit="OnValidSubmitAsync">
<DataAnnotationsValidator/>
<ValidationSummary class="text-danger" role="alert"/>
<div class="form-floating mb-3">
<input type="text" value="@username" class="form-control" placeholder="Please choose your username." disabled/>
<label for="username" class="form-label">Username</label>
<ValidationSummary class="text-red-500 mb-4" role="alert"/>
<div class="mb-4">
<label for="username" class="block text-sm font-medium text-gray-700 mb-1">Username</label>
<input type="text" value="@username" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-gray-100 cursor-not-allowed" placeholder="Please choose your username." disabled/>
</div>
<div class="form-floating mb-3">
<InputText @bind-Value="Input.PhoneNumber" class="form-control" placeholder="Please enter your phone number."/>
<label for="phone-number" class="form-label">Phone number</label>
<ValidationMessage For="() => Input.PhoneNumber" class="text-danger"/>
<div class="mb-4">
<label for="phone-number" class="block text-sm font-medium text-gray-700 mb-1">Phone number</label>
<InputText @bind-Value="Input.PhoneNumber" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" placeholder="Please enter your phone number."/>
<ValidationMessage For="() => Input.PhoneNumber" class="text-red-500 text-sm mt-1"/>
</div>
<button type="submit" class="w-100 btn btn-lg btn-primary">Save</button>
<button type="submit" class="w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-md transition duration-150 ease-in-out">Save</button>
</EditForm>
</div>
</div>

View File

@@ -2,16 +2,24 @@
@using AliasVault.Admin2.Main.Layout
@layout MainLayout
<h1>Manage your account</h1>
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<GlobalNotificationDisplay />
<div>
<h2>Change your account settings</h2>
<hr/>
<div class="row">
<div class="col-md-3">
<div class="flex items-center justify-between">
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Manage account</h1>
</div>
<p>Manage your profile here.</p>
</div>
</div>
<div class="container mx-auto px-4 py-8">
<hr class="mb-6 border-t border-gray-300"/>
<div class="flex flex-col md:flex-row">
<div class="w-full md:w-1/4 mb-6 md:mb-0">
<ManageNavMenu/>
</div>
<div class="col-md-9">
<div class="w-full md:w-3/4 md:pl-8">
@Body
</div>
</div>

View File

@@ -2,27 +2,27 @@
@inject SignInManager<AdminUser> SignInManager
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<NavLink class="nav-link" href="account/manage" Match="NavLinkMatch.All">Profile</NavLink>
<ul class="flex flex-col space-y-1">
<li>
<NavLink href="account/manage" Match="NavLinkMatch.All" class="block px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-gray-900 transition-colors duration-150">Profile</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="account/manage/Email">Email</NavLink>
<li>
<NavLink href="account/manage/Email" class="block px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-gray-900 transition-colors duration-150">Email</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="account/manage/ChangePassword">Password</NavLink>
<li>
<NavLink href="account/manage/ChangePassword" class="block px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-gray-900 transition-colors duration-150">Password</NavLink>
</li>
@if (hasExternalLogins)
{
<li class="nav-item">
<NavLink class="nav-link" href="account/manage/ExternalLogins">External logins</NavLink>
</li>
}
<li class="nav-item">
<NavLink class="nav-link" href="account/manage/TwoFactorAuthentication">Two-factor authentication</NavLink>
<li>
<NavLink href="account/manage/ExternalLogins" class="block px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-gray-900 transition-colors duration-150">External logins</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="account/manage/PersonalData">Personal data</NavLink>
}
<li>
<NavLink href="account/manage/TwoFactorAuthentication" class="block px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-gray-900 transition-colors duration-150">Two-factor authentication</NavLink>
</li>
<li>
<NavLink href="account/manage/PersonalData" class="block px-4 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-gray-900 transition-colors duration-150">Personal data</NavLink>
</li>
</ul>

View File

@@ -2,7 +2,15 @@
<PageTitle>Emails</PageTitle>
<h1>Emails</h1>
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems" />
<div class="flex items-center justify-between">
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Emails</h1>
</div>
<p>This page gives an overview of recently received mails by this AliasVault server.</p>
</div>
</div>
@if (IsLoading)
{
@@ -12,14 +20,44 @@
}
else
{
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-4">
@foreach (var email in EmailList)
{
<div>
<div>@email.From</div>
<div>@email.MessagePreview</div>
</div>
}
<div class="overflow-x-auto shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left text-gray-500">
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
<tr>
<th scope="col" class="px-4 py-3">ID</th>
<th scope="col" class="px-4 py-3">Subject</th>
<th scope="col" class="px-4 py-3">From</th>
<th scope="col" class="px-4 py-3">To</th>
<th scope="col" class="px-4 py-3">Date</th>
<th scope="col" class="px-4 py-3">System Date</th>
<th scope="col" class="px-4 py-3">Preview</th>
<th scope="col" class="px-4 py-3">Attachments</th>
</tr>
</thead>
<tbody>
@foreach (var email in EmailList)
{
<tr class="bg-white border-b hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-900">@email.Id</td>
<td class="px-4 py-3">@email.Subject</td>
<td class="px-4 py-3">
<span class="font-medium">@email.FromLocal</span>@@@email.FromDomain
</td>
<td class="px-4 py-3">
<span class="font-medium">@email.ToLocal</span>@@@email.ToDomain
</td>
<td class="px-4 py-3">@email.Date.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">@email.DateSystem.ToString("yyyy-MM-dd HH:mm")</td>
<td class="px-4 py-3">
<span class="line-clamp-1">@email.MessagePreview</span>
</td>
<td class="px-4 py-3">
@email.Attachments.Count
</td>
</tr>
}
</tbody>
</table>
</div>
}
@@ -43,6 +81,7 @@ else
// Load the aliases from the database.
var user = UserService.User();
EmailList = await DbContext.Emails
.OrderByDescending(x => x.DateSystem)
.ToListAsync();
IsLoading = false;

View File

@@ -3,8 +3,13 @@
<PageTitle>Home</PageTitle>
<h1>AliasVault Admin</h1>
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems" />
<div class="flex items-center justify-between">
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">AliasVault Admin</h1>
</div>
<p>Welcome to the AliasVault admin portal.</p>
</div>
</div>
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
Welcome to the AliasVault admin portal.

View File

@@ -118,4 +118,13 @@ public class MainBase : OwningComponentBase
}
}
}
/// <summary>
/// Gets the username from the authentication state asynchronously.
/// </summary>
/// <returns>The username.</returns>
protected string GetUsername()
{
return UserService.User().UserName ?? "[Unknown]";
}
}

View File

@@ -15,6 +15,7 @@
@using AliasVault.Admin2.Main.Components.Alerts
@using AliasVault.Admin2.Main.Components.Layout
@using AliasVault.Admin2.Main.Models
@using AliasVault.Admin2.Main.Pages
@using AliasVault.Admin2.Services
@using AliasServerDb
@using Microsoft.AspNetCore.Authorization

1425
src/AliasVault.Admin2/package-lock.json generated Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
{
"name": "aliasvault.client",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build:css": "tailwindcss -i ./tailwind.css -o ./wwwroot/css/tailwind.css --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1,57 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./**/*.html',
'./**/*.razor',
],
safelist: [
'w-64',
'w-1/2',
'rounded-l-lg',
'rounded-r-lg',
'bg-gray-200',
'grid-cols-4',
'grid-cols-7',
'h-6',
'leading-6',
'h-9',
'leading-9',
'shadow-lg',
'bg-opacity-50',
'dark:bg-opacity-80'
],
darkMode: "class",
theme: {
extend: {
colors: {
primary: {
"900": "#7b4a1e",
"800": "#9a5d26",
"700": "#b8702f",
"600": "#d68338",
"500": "#f49541",
"400": "#f6a752",
"300": "#f8b963",
"200": "#fbcb74",
"100": "#fdde85",
"50": "#ffe096"
}
},
fontFamily: {
'sans': ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'system-ui', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'Noto Sans', 'sans-serif', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'],
'body': ['Inter', 'ui-sans-serif', 'system-ui', '-apple-system', 'system-ui', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'Noto Sans', 'sans-serif', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'],
'mono': ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', 'monospace']
},
transitionProperty: {
'width': 'width'
},
textDecoration: ['active'],
minWidth: {
'kanban': '28rem'
},
},
},
plugins: [
],
}

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -2,16 +2,6 @@ html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}

View File

File diff suppressed because it is too large Load Diff

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 936 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@@ -0,0 +1,53 @@
function initDarkModeSwitcher() {
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
if (themeToggleDarkIcon === null && themeToggleLightIcon === null) {
return;
}
if (localStorage.getItem('color-theme')) {
if (localStorage.getItem('color-theme') === 'light') {
document.documentElement.classList.remove('dark');
themeToggleLightIcon.classList.remove('hidden');
} else {
document.documentElement.classList.add('dark');
themeToggleDarkIcon.classList.remove('hidden');
}
}
else {
// Default to light mode if not set.
document.documentElement.classList.remove('dark');
themeToggleLightIcon.classList.remove('hidden');
}
const themeToggleBtn = document.getElementById('theme-toggle');
let event = new Event('dark-mode');
themeToggleBtn.addEventListener('click', function () {
// toggle icons
themeToggleDarkIcon.classList.toggle('hidden');
themeToggleLightIcon.classList.toggle('hidden');
// if set via local storage previously
if (localStorage.getItem('color-theme')) {
if (localStorage.getItem('color-theme') === 'light') {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
}
// if NOT set via local storage previously
} else if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
document.dispatchEvent(event);
});
}

View File

@@ -0,0 +1,11 @@
function downloadFileFromStream(fileName, contentStreamReference) {
const arrayBuffer = new Uint8Array(contentStreamReference).buffer;
const blob = new Blob([arrayBuffer]);
const url = URL.createObjectURL(blob);
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.download = fileName ?? '';
anchorElement.click();
anchorElement.remove();
URL.revokeObjectURL(url);
}