everything related to blazor!
This commit is contained in:
parent
700a06e7e0
commit
211d3fdc03
@ -93,7 +93,7 @@ namespace Api.Controllers
|
|||||||
public async Task<IActionResult> AddComment(Guid id, AddCommentViewModel command)
|
public async Task<IActionResult> AddComment(Guid id, AddCommentViewModel command)
|
||||||
{
|
{
|
||||||
var commentId = await _bus.Send(new CommentLinkCommand(id, command.Content));
|
var commentId = await _bus.Send(new CommentLinkCommand(id, command.Content));
|
||||||
return CreatedAtAction("", "", new { id = commentId }, null);
|
return Created($"comments/{commentId}", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -114,7 +114,12 @@ namespace Api
|
|||||||
app.UseSwaggerUi3();
|
app.UseSwaggerUi3();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseCors(o => o.AllowAnyOrigin());
|
app.UseCors(o =>
|
||||||
|
{
|
||||||
|
o.AllowAnyOrigin();
|
||||||
|
o.AllowAnyMethod();
|
||||||
|
o.WithHeaders("content-type", "authorization");
|
||||||
|
});
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<Router AppAssembly="@typeof(Program).Assembly">
|
<CascadingAuthenticationState>
|
||||||
|
<Router AppAssembly="@typeof(Program).Assembly">
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||||
</Found>
|
</Found>
|
||||||
<NotFound>
|
<NotFound>
|
||||||
<LayoutView Layout="@typeof(MainLayout)">
|
<LayoutView Layout="@typeof(MainLayout)">
|
||||||
@ -8,3 +9,4 @@
|
|||||||
</LayoutView>
|
</LayoutView>
|
||||||
</NotFound>
|
</NotFound>
|
||||||
</Router>
|
</Router>
|
||||||
|
</CascadingAuthenticationState>
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="5.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.0" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.0" PrivateAssets="all" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
|
|||||||
91
Apps/Client/CustomAuthStateProvider.cs
Normal file
91
Apps/Client/CustomAuthStateProvider.cs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
|
||||||
|
namespace Client
|
||||||
|
{
|
||||||
|
public sealed class CustomAuthStateProvider : AuthenticationStateProvider
|
||||||
|
{
|
||||||
|
private static string _token;
|
||||||
|
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
|
||||||
|
public CustomAuthStateProvider(HttpClient http)
|
||||||
|
{
|
||||||
|
_http = http;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||||
|
{
|
||||||
|
return Task.FromResult(AuthenticationStateFromCurrentToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkUserAsAuthenticated(string token)
|
||||||
|
{
|
||||||
|
_token = token;
|
||||||
|
NotifyAuthenticationStateChanged(Task.FromResult(AuthenticationStateFromCurrentToken()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthenticationState AuthenticationStateFromCurrentToken()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_token))
|
||||||
|
{
|
||||||
|
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||||
|
}
|
||||||
|
|
||||||
|
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token);
|
||||||
|
|
||||||
|
var principal = new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(_token), "jwt", "unique_name", null));
|
||||||
|
|
||||||
|
return new AuthenticationState(principal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>();
|
||||||
|
var payload = jwt.Split('.')[1];
|
||||||
|
var jsonBytes = ParseBase64WithoutPadding(payload);
|
||||||
|
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
|
||||||
|
|
||||||
|
keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles);
|
||||||
|
|
||||||
|
if (roles != null)
|
||||||
|
{
|
||||||
|
if (roles.ToString().Trim().StartsWith("["))
|
||||||
|
{
|
||||||
|
var parsedRoles = JsonSerializer.Deserialize<string[]>(roles.ToString());
|
||||||
|
|
||||||
|
foreach (var parsedRole in parsedRoles)
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(ClaimTypes.Role, parsedRole));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(ClaimTypes.Role, roles.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
keyValuePairs.Remove(ClaimTypes.Role);
|
||||||
|
}
|
||||||
|
|
||||||
|
claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] ParseBase64WithoutPadding(string base64)
|
||||||
|
{
|
||||||
|
switch (base64.Length % 4)
|
||||||
|
{
|
||||||
|
case 2: base64 += "=="; break;
|
||||||
|
case 3: base64 += "="; break;
|
||||||
|
}
|
||||||
|
return Convert.FromBase64String(base64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Apps/Client/NotificationManager.cs
Normal file
36
Apps/Client/NotificationManager.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Client
|
||||||
|
{
|
||||||
|
public sealed class NotificationManager
|
||||||
|
{
|
||||||
|
private readonly ILogger<NotificationManager> _logger;
|
||||||
|
private Queue<string> _messages = new Queue<string>();
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Messages => _messages.ToArray();
|
||||||
|
|
||||||
|
public event Action OnChange;
|
||||||
|
|
||||||
|
public NotificationManager(ILogger<NotificationManager> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(string message)
|
||||||
|
{
|
||||||
|
_messages.Enqueue(message);
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Task.Delay(5000);
|
||||||
|
_messages.Dequeue();
|
||||||
|
OnChange?.Invoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
OnChange?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,8 +2,10 @@
|
|||||||
@inject MyState State
|
@inject MyState State
|
||||||
|
|
||||||
@foreach (var name in State.Names)
|
@foreach (var name in State.Names)
|
||||||
|
|
||||||
{
|
{
|
||||||
<p>@name</p>
|
<p>@name</p>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<Names />
|
<Names />
|
||||||
@ -12,11 +14,18 @@
|
|||||||
<button @onclick="OnSend">Save</button>
|
<button @onclick="OnSend">Save</button>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
private string CurrentValue;
|
private string CurrentValue;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void OnSend()
|
private void OnSend()
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
||||||
State.AddName(CurrentValue);
|
State.AddName(CurrentValue);
|
||||||
|
|
||||||
CurrentValue = "";
|
CurrentValue = "";
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@inject LinksClient Links
|
@inject LinksClient Links
|
||||||
|
|
||||||
<h1>Latest links</h1>
|
<Title Value="Latest links" />
|
||||||
|
|
||||||
@if (_loading)
|
@if (_links == null)
|
||||||
{
|
{
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
}
|
}
|
||||||
@ -20,20 +20,10 @@ else
|
|||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private LinkDto[] _links = new LinkDto[] { };
|
private LinkDto[] _links;
|
||||||
private bool _loading = false;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
|
||||||
_loading = true;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
_links = (await Links.GetLinksAsync()).ToArray();
|
_links = (await Links.GetLinksAsync()).ToArray();
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
_loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
36
Apps/Client/Pages/Login.razor
Normal file
36
Apps/Client/Pages/Login.razor
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
@page "/login"
|
||||||
|
@inject AccountsClient Accounts
|
||||||
|
@inject NotificationManager Notifications
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject AuthenticationStateProvider AuthStateProvider
|
||||||
|
|
||||||
|
<Title Value="Sign in!" />
|
||||||
|
|
||||||
|
<EditForm Model="@_model" OnValidSubmit="TryLogin">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
<ValidationSummary />
|
||||||
|
|
||||||
|
<InputText @bind-Value="_model.Username" />
|
||||||
|
<InputText type="password" @bind-Value="_model.Password" />
|
||||||
|
|
||||||
|
<button type="submit">Log in!</button>
|
||||||
|
</EditForm>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private LoginViewModel _model = new LoginViewModel();
|
||||||
|
|
||||||
|
private async Task TryLogin()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var token = await Accounts.LoginAsync(_model);
|
||||||
|
((CustomAuthStateProvider)AuthStateProvider).MarkUserAsAuthenticated(token);
|
||||||
|
Navigation.NavigateTo("/");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Notifications.Add("login failed!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
@page "/links/{id:guid}"
|
@page "/links/{id:guid}"
|
||||||
@inject LinksClient Links
|
@inject LinksClient Links
|
||||||
|
@inject NotificationManager Notifications
|
||||||
|
|
||||||
@if (Item == null)
|
@if (Item == null)
|
||||||
{
|
{
|
||||||
@ -7,7 +8,29 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<h1>Showing link @Item.Url with @Item.UpVotes / @Item.DownVotes</h1>
|
<h1>@Item.Url</h1>
|
||||||
|
<div>
|
||||||
|
👍 @Item.UpVotes
|
||||||
|
👎 @Item.DownVotes
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (Comments == null)
|
||||||
|
{
|
||||||
|
<p>Loading comments ...</p>
|
||||||
|
} else if(Comments.Count == 0)
|
||||||
|
{
|
||||||
|
<p>No comment yet!</p>
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
foreach (var comment in Comments)
|
||||||
|
{
|
||||||
|
<Comment @key="comment.Id" Item="@comment" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<AuthorizeView>
|
||||||
|
<CommentForm OnSubmit="@SubmitComment" Username="@context.User.Identity.Name" />
|
||||||
|
</AuthorizeView>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
@ -16,9 +39,30 @@ else
|
|||||||
|
|
||||||
private LinkDto Item;
|
private LinkDto Item;
|
||||||
|
|
||||||
|
private ICollection<CommentDto> Comments;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
Item = await Links.GetLinkByIdAsync(Id);
|
Item = await Links.GetLinkByIdAsync(Id);
|
||||||
|
await FetchComments();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async Task FetchComments()
|
||||||
|
{
|
||||||
|
Comments = await Links.CommentsAsync(Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SubmitComment(AddCommentViewModel command)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Links.AddCommentAsync(Id, command);
|
||||||
|
await FetchComments();
|
||||||
|
Notifications.Add("comment added!");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Notifications.Add("could not post a comment");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
|
|
||||||
namespace Client
|
namespace Client
|
||||||
{
|
{
|
||||||
@ -27,9 +28,13 @@ namespace Client
|
|||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
builder.RootComponents.Add<App>("#app");
|
builder.RootComponents.Add<App>("#app");
|
||||||
|
|
||||||
|
builder.Services.AddAuthorizationCore();
|
||||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("http://localhost:8888/") });
|
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("http://localhost:8888/") });
|
||||||
builder.Services.AddScoped<LinksClient>();
|
builder.Services.AddScoped<LinksClient>();
|
||||||
|
builder.Services.AddScoped<AccountsClient>();
|
||||||
builder.Services.AddSingleton<MyState>();
|
builder.Services.AddSingleton<MyState>();
|
||||||
|
builder.Services.AddSingleton<NotificationManager>();
|
||||||
|
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
await builder.Build().RunAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
7
Apps/Client/Shared/Comment.razor
Normal file
7
Apps/Client/Shared/Comment.razor
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<p>@Item.Content</p>
|
||||||
|
<p>- @Item.CreatedByName</p>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter]
|
||||||
|
public CommentDto Item { get; set; }
|
||||||
|
}
|
||||||
35
Apps/Client/Shared/CommentForm.razor
Normal file
35
Apps/Client/Shared/CommentForm.razor
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<EditForm Model="@_model" OnValidSubmit="@OnValidSubmit">
|
||||||
|
<DataAnnotationsValidator />
|
||||||
|
|
||||||
|
<InputText @bind-Value="_model.Content" />
|
||||||
|
<ValidationMessage For=@(() => _model.Content) />
|
||||||
|
|
||||||
|
<button type="submit">Comment as @_username</button>
|
||||||
|
</EditForm>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private AddCommentViewModel _model = new AddCommentViewModel();
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState> authenticationStateTask { get; set; }
|
||||||
|
|
||||||
|
private string _username;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<AddCommentViewModel> OnSubmit { get; set; }
|
||||||
|
|
||||||
|
protected async override Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
_username = (await authenticationStateTask).User.Identity.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnValidSubmit()
|
||||||
|
{
|
||||||
|
await OnSubmit.InvokeAsync(_model);
|
||||||
|
_model = new AddCommentViewModel();
|
||||||
|
}
|
||||||
|
}
|
||||||
5
Apps/Client/Shared/EmptyLayout.razor
Normal file
5
Apps/Client/Shared/EmptyLayout.razor
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<main>
|
||||||
|
@Body
|
||||||
|
</main>
|
||||||
7
Apps/Client/Shared/Inside.razor
Normal file
7
Apps/Client/Shared/Inside.razor
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<p>Title was @MyValue</p>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[CascadingParameter]
|
||||||
|
private string MyValue { get; set; }
|
||||||
|
}
|
||||||
@ -1,17 +1,22 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="sidebar">
|
<div class="topbar">
|
||||||
<NavMenu />
|
<NavLink href="/">Home</NavLink>
|
||||||
|
|
||||||
|
<AuthorizeView>
|
||||||
|
<Authorized>
|
||||||
|
Sign in as @context.User.Identity.Name
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
You're not signed in <NavLink href="/login">Sign in</NavLink>.
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main">
|
<main>
|
||||||
<div class="top-row px-4">
|
|
||||||
<a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content px-4">
|
|
||||||
@Body
|
@Body
|
||||||
</div>
|
</main>
|
||||||
</div>
|
|
||||||
|
<Toasts />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,70 +0,0 @@
|
|||||||
.page {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row {
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
border-bottom: 1px solid #d6d5d5;
|
|
||||||
justify-content: flex-end;
|
|
||||||
height: 3.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row ::deep a, .top-row .btn-link {
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row a:first-child {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640.98px) {
|
|
||||||
.top-row:not(.auth) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row.auth {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row a, .top-row .btn-link {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 641px) {
|
|
||||||
.page {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 250px;
|
|
||||||
height: 100vh;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main > div {
|
|
||||||
padding-left: 2rem !important;
|
|
||||||
padding-right: 1.5rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,6 @@
|
|||||||
@inject MyState State
|
@inject MyState State
|
||||||
|
@implements IDisposable
|
||||||
|
@inject IJSRuntime JS;
|
||||||
|
|
||||||
<p>Got @State.Names.Count</p>
|
<p>Got @State.Names.Count</p>
|
||||||
|
|
||||||
@ -10,6 +12,7 @@
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
JS.InvokeVoidAsync("alert", "Disposed");
|
||||||
State.OnChange -= StateHasChanged;
|
State.OnChange -= StateHasChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,7 +5,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
|
<div @onclick="ToggleNavMenu">
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item px-3">
|
<li class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||||
|
|||||||
@ -1,62 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oi {
|
|
||||||
width: 2rem;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
vertical-align: text-top;
|
|
||||||
top: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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.25);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
Apps/Client/Shared/Title.razor
Normal file
14
Apps/Client/Shared/Title.razor
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
@inject IJSRuntime JS
|
||||||
|
|
||||||
|
<h1>@Value</h1>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public string Value { get; set; }
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
JS.InvokeVoidAsync("setTitle", Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Apps/Client/Shared/Title.razor.css
Normal file
3
Apps/Client/Shared/Title.razor.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
h1 {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
20
Apps/Client/Shared/Toasts.razor
Normal file
20
Apps/Client/Shared/Toasts.razor
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
@inject NotificationManager Notifications
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
@foreach (var message in Notifications.Messages)
|
||||||
|
{
|
||||||
|
<p>⚡ @message</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Notifications.OnChange += StateHasChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Notifications.OnChange -= StateHasChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@
|
|||||||
@using Microsoft.AspNetCore.Components.Web
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using Client
|
@using Client
|
||||||
@using Client.Shared
|
@using Client.Shared
|
||||||
|
|||||||
@ -1,24 +1,9 @@
|
|||||||
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
|
html,
|
||||||
|
body {
|
||||||
html, body {
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a, .btn-link {
|
.valid.modified:not([type="checkbox"]) {
|
||||||
color: #0366d6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #1b6ec2;
|
|
||||||
border-color: #1861ac;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding-top: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.valid.modified:not([type=checkbox]) {
|
|
||||||
outline: 1px solid #26b050;
|
outline: 1px solid #26b050;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
<title>Client</title>
|
<title>Client</title>
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||||
@ -20,6 +22,9 @@
|
|||||||
<a class="dismiss">🗙</a>
|
<a class="dismiss">🗙</a>
|
||||||
</div>
|
</div>
|
||||||
<script src="_framework/blazor.webassembly.js"></script>
|
<script src="_framework/blazor.webassembly.js"></script>
|
||||||
</body>
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.setTitle = (title) => (document.title = title);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
10
README.md
10
README.md
@ -180,6 +180,16 @@ project.csproj
|
|||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
## Blazor
|
||||||
|
|
||||||
|
Ajout de l'authentification :
|
||||||
|
|
||||||
|
Ajout du paquet `Microsoft.AspNetCore.Components.Authorization` et du using dans `_Imports.razor`.
|
||||||
|
|
||||||
|
Ensuite, Passage par `AuthorizeRouteView` dans le fichier `App.razor`.
|
||||||
|
|
||||||
|
Ensuite, ajout d'un `AuthenticationStateProvider` custom pour déterminer si l'utilisateur est connecté ou non.
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
On build à la racine de la solution avec `docker build -f .\Apps\Website\Dockerfile -t hn .`.
|
On build à la racine de la solution avec `docker build -f .\Apps\Website\Dockerfile -t hn .`.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user