Initial code commit.
This commit is contained in:
12
ui/Sufi.Demo.PeopleDirectory.UI/Client/App.razor
Normal file
12
ui/Sufi.Demo.PeopleDirectory.UI/Client/App.razor
Normal file
@@ -0,0 +1,12 @@
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
210
ui/Sufi.Demo.PeopleDirectory.UI/Client/Pages/Contacts.razor
Normal file
210
ui/Sufi.Demo.PeopleDirectory.UI/Client/Pages/Contacts.razor
Normal file
@@ -0,0 +1,210 @@
|
||||
@page "/contacts"
|
||||
|
||||
@inject HttpClient Http
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject BoomerangService Boomerang
|
||||
|
||||
<PageTitle>Contact List</PageTitle>
|
||||
|
||||
<MudGrid Class="py-4">
|
||||
<MudItem xs="12">
|
||||
<MudText Typo="Typo.h3">Contact List</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudText Typo="Typo.body1">Behold! Below is the list of all registered users in this application.</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Error">(All data will be deleted for every 10 minutes)</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12" Class="d-flex gap-4">
|
||||
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="OnButtonCreateClicked">Create</MudButton>
|
||||
|
||||
<MudSpacer />
|
||||
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Refresh" OnClick="OnButtonRefreshClicked">Refresh</MudButton>
|
||||
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
@if (contacts != null && contacts.Length > 0)
|
||||
{
|
||||
<MudTable Items="@contacts" Hover Breakpoint="Breakpoint.Sm" Loading="@loading" LoadingProgressColor="Color.Info" Class="py-3">
|
||||
<HeaderContent>
|
||||
<MudTh>Id</MudTh>
|
||||
<MudTh>Username</MudTh>
|
||||
<MudTh>Phone</MudTh>
|
||||
<MudTh>Email</MudTh>
|
||||
<MudTh>Skill Sets</MudTh>
|
||||
<MudTh>Hobby</MudTh>
|
||||
<MudTh>Action</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Id">@context.Id</MudTd>
|
||||
<MudTd DataLabel="Username">@context.UserName</MudTd>
|
||||
<MudTd DataLabel="Phone">@context.Phone</MudTd>
|
||||
<MudTd DataLabel="Email">@context.Email</MudTd>
|
||||
<MudTd DataLabel="Skill Sets">@context.SkillSets</MudTd>
|
||||
<MudTd DataLabel="Hobby">@context.Hobby</MudTd>
|
||||
<MudTd>
|
||||
<MudTooltip Text="Edit" Placement="Placement.Top" Arrow>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" OnClick="() => OnEditContactClicked(context.Id)" Color="Color.Warning" Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="Delete" Placement="Placement.Top" Arrow>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" OnClick="() => OnDeleteContactClicked(context.Id)" Color="Color.Error" Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
<PagerContent>
|
||||
<MudTablePager />
|
||||
</PagerContent>
|
||||
</MudTable>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.h5">No item to display. Click on the 'Create' button to add.</MudText>
|
||||
}
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudOverlay Visible="loading" DarkBackground ZIndex="9999">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate />
|
||||
</MudOverlay>
|
||||
|
||||
@code {
|
||||
private GetAllContactsResponse[]? contacts;
|
||||
private bool loading;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadTableAsync();
|
||||
|
||||
await Boomerang.AddVariableAsync("PageName", "Contacts");
|
||||
await Boomerang.SendBeaconAsync();
|
||||
}
|
||||
|
||||
private async Task LoadTableAsync()
|
||||
{
|
||||
loading = true;
|
||||
|
||||
var response = await Http.GetAsync("api/v1/contacts");
|
||||
var result = await response.ToResult<List<GetAllContactsResponse>>();
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
contacts = result.Data.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
contacts = null;
|
||||
|
||||
foreach (var message in result.Messages)
|
||||
{
|
||||
Snackbar.Add(message, Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
private async Task OnButtonRefreshClicked()
|
||||
{
|
||||
await LoadTableAsync();
|
||||
}
|
||||
|
||||
private async Task OnButtonCreateClicked()
|
||||
{
|
||||
var dialogOptions = new DialogOptions { BackdropClick = false, CloseButton = true };
|
||||
var dialog = await DialogService.ShowAsync<CreateContactDialog>("Create New Contact", dialogOptions);
|
||||
var dialogResult = await dialog.Result;
|
||||
|
||||
if (dialogResult!.Canceled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var contactData = (AddEditContactCommand)dialogResult.Data!;
|
||||
var response = await Http.PostAsJsonAsync("api/v1/contacts", contactData);
|
||||
var result = await response.ToResult<int>();
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
Snackbar.Add(result.Messages[0], Severity.Success);
|
||||
await LoadTableAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var message in result.Messages)
|
||||
{
|
||||
Snackbar.Add(message, Severity.Error);
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async Task OnEditContactClicked(int id)
|
||||
{
|
||||
var dialogParams = new DialogParameters { ["Id"] = id };
|
||||
var dialogOptions = new DialogOptions { BackdropClick = false, CloseButton = true };
|
||||
var dialog = await DialogService.ShowAsync<EditContactDialog>("Edit Contact", dialogParams, dialogOptions);
|
||||
var dialogResult = await dialog.Result;
|
||||
if (dialogResult!.Canceled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var contactData = (AddEditContactCommand)dialogResult.Data!;
|
||||
var response = await Http.PostAsJsonAsync("api/v1/contacts", contactData);
|
||||
var result = await response.ToResult<int>();
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
Snackbar.Add(result.Messages[0], Severity.Success);
|
||||
await LoadTableAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var message in result.Messages)
|
||||
{
|
||||
Snackbar.Add(message, Severity.Error);
|
||||
}
|
||||
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
async Task OnDeleteContactClicked(int id)
|
||||
{
|
||||
var dialogResult = await DialogService.ShowMessageBox("Delete Contact", "Confirm to delete the item? This action cannot be undone.", yesText: "Delete!", cancelText: "No");
|
||||
if (dialogResult == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var deleteResponse = await Http.DeleteAsync($"api/v1/contacts/{id}");
|
||||
if (deleteResponse.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await deleteResponse.ToResult();
|
||||
if (result!.Succeeded)
|
||||
{
|
||||
Snackbar.Add("Contact deleted successfully.", Severity.Success);
|
||||
await LoadTableAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(result.Messages[0], Severity.Error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add("An error has occurred.", Severity.Error);
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudForm @ref="form" Model="request" @bind-IsValid="success" @bind-Errors="errors">
|
||||
<MudTextField Label="Username" Required @bind-Value="request.UserName" />
|
||||
<MudTextField Label="Email" Required @bind-Value="request.Email" />
|
||||
<MudTextField Label="Phone" Required @bind-Value="request.Phone" MaxLength="20" />
|
||||
<MudTextField Label="Skill Sets" Required @bind-Value="request.SkillSets" />
|
||||
<MudTextField Label="Hobby" Required @bind-Value="request.Hobby" />
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">Cancel</MudButton>
|
||||
<MudButton OnClick="Submit">Ok</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
private static readonly string[] SampleUserNames = new[]
|
||||
{
|
||||
"alex99",
|
||||
"samwise",
|
||||
"lunaStar",
|
||||
"maverick",
|
||||
"nova",
|
||||
"pixelPro",
|
||||
"kanu",
|
||||
"tay",
|
||||
"zorin",
|
||||
"echo"
|
||||
};
|
||||
private static readonly Random _rnd = new();
|
||||
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
private AddEditContactCommand request = new();
|
||||
private MudForm? form;
|
||||
private bool success;
|
||||
private string[] errors = { };
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
await form!.Validate();
|
||||
if (!success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MudDialog.Close(request);
|
||||
}
|
||||
|
||||
protected override Task OnInitializedAsync()
|
||||
{
|
||||
// Assign a random username from the in-memory list when the dialog opens
|
||||
request.UserName = SampleUserNames[_rnd.Next(SampleUserNames.Length)];
|
||||
return base.OnInitializedAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
@using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetById
|
||||
|
||||
@inject HttpClient Http
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@if (showAlert)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error">@alertMessage</MudAlert>
|
||||
}
|
||||
<MudForm @ref="form" Model="request" @bind-IsValid="success" @bind-Errors="errors">
|
||||
<MudTextField Label="Username" Required="true" @bind-Value="request.UserName" />
|
||||
<MudTextField Label="Email" Required="true" @bind-Value="request.Email" />
|
||||
<MudTextField Label="Phone" Required="true" @bind-Value="request.Phone" />
|
||||
<MudTextField Label="Skill Sets" Required="true" @bind-Value="request.SkillSets" />
|
||||
<MudTextField Label="Hobby" Required="true" @bind-Value="request.Hobby" />
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">Cancel</MudButton>
|
||||
<MudButton OnClick="Submit">Ok</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter]
|
||||
public int Id { get; set; }
|
||||
|
||||
private AddEditContactCommand request = new();
|
||||
private MudForm? form;
|
||||
private bool success;
|
||||
private string[] errors = { };
|
||||
private bool showAlert;
|
||||
private string? alertMessage;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var response = await Http.GetAsync($"api/v1/contacts/{Id}");
|
||||
var result = await response.ToResult<GetContactByIdResponse>();
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var contact = result.Data;
|
||||
request.Id = Id;
|
||||
request.UserName = contact!.UserName;
|
||||
request.Email = contact.Email;
|
||||
request.Phone = contact.Phone;
|
||||
request.SkillSets = contact.SkillSets;
|
||||
request.Hobby = contact.Hobby;
|
||||
|
||||
showAlert = false;
|
||||
alertMessage = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
showAlert = true;
|
||||
alertMessage = string.Join(',', result.Messages);
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel() => MudDialog.Cancel();
|
||||
|
||||
private async Task Submit()
|
||||
{
|
||||
await form!.Validate();
|
||||
if (!success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MudDialog!.Close(request);
|
||||
}
|
||||
}
|
||||
22
ui/Sufi.Demo.PeopleDirectory.UI/Client/Pages/Index.razor
Normal file
22
ui/Sufi.Demo.PeopleDirectory.UI/Client/Pages/Index.razor
Normal file
@@ -0,0 +1,22 @@
|
||||
@page "/"
|
||||
|
||||
@inject BoomerangService Boomerang
|
||||
|
||||
<PageTitle>Index - Demo App</PageTitle>
|
||||
|
||||
<MudGrid Class="py-4">
|
||||
<MudItem xs="12">
|
||||
<MudText Typo="Typo.h3">Welcome to this demo app.</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudText Typo="Typo.body1">Please view the contact list page.</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
@code {
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await Boomerang.AddVariableAsync("PageName", "Index");
|
||||
await Boomerang.SendBeaconAsync();
|
||||
}
|
||||
}
|
||||
25
ui/Sufi.Demo.PeopleDirectory.UI/Client/Program.cs
Normal file
25
ui/Sufi.Demo.PeopleDirectory.UI/Client/Program.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using MudBlazor;
|
||||
using MudBlazor.Services;
|
||||
using Sufi.Demo.PeopleDirectory.UI.Client;
|
||||
using Sufi.Demo.PeopleDirectory.UI.Client.Services;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||
|
||||
// MudBlazor
|
||||
builder.Services.AddMudServices(config =>
|
||||
{
|
||||
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.TopRight;
|
||||
config.SnackbarConfiguration.ClearAfterNavigation = false;
|
||||
config.SnackbarConfiguration.HideTransitionDuration = 200;
|
||||
config.SnackbarConfiguration.ShowTransitionDuration = 200;
|
||||
config.SnackbarConfiguration.NewestOnTop = true;
|
||||
});
|
||||
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||
builder.Services.AddScoped<BoomerangService>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:50212",
|
||||
"sslPort": 44358
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"Sufi.Demo.PeopleDirectory.UI": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "https://localhost:7024;http://localhost:5243",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Client.Services
|
||||
{
|
||||
public class BoomerangService(
|
||||
IJSRuntime jsRuntime
|
||||
)
|
||||
{
|
||||
public async Task<bool> InitializeAsync(object config)
|
||||
{
|
||||
return await jsRuntime.InvokeAsync<bool>("boomerangHelper.init", config);
|
||||
}
|
||||
|
||||
public async Task AddVariableAsync(string key, string value)
|
||||
{
|
||||
await jsRuntime.InvokeVoidAsync("boomerangHelper.addVar", key, value);
|
||||
}
|
||||
|
||||
public async Task SendBeaconAsync()
|
||||
{
|
||||
await jsRuntime.InvokeVoidAsync("boomerangHelper.sendBeacon");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<MudThemeProvider />
|
||||
<MudPopoverProvider />
|
||||
<MudDialogProvider MaxWidth="MaxWidth.ExtraSmall" FullWidth="true" />
|
||||
<MudSnackbarProvider />
|
||||
|
||||
<MudLayout>
|
||||
<MudAppBar Color="Color.Info">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@(e => ToggleDrawer())" />
|
||||
<MudSpacer />
|
||||
</MudAppBar>
|
||||
<MudDrawer @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Never">
|
||||
<SideNavMenu />
|
||||
</MudDrawer>
|
||||
<MudMainContent>
|
||||
<MudContainer MaxWidth="MaxWidth.False">
|
||||
@Body
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
|
||||
@code {
|
||||
bool _drawerOpen = true;
|
||||
|
||||
void ToggleDrawer()
|
||||
{
|
||||
_drawerOpen = !_drawerOpen;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
.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 ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep 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 ::deep a, .top-row ::deep .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;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<MudNavMenu Bordered>
|
||||
<MudText Typo="Typo.h6" Class="px-4 pt-3">Sufi Demo App</MudText>
|
||||
<MudText Typo="Typo.body2" Class="px-4 mud-text-secondary">For demo only</MudText>
|
||||
<MudDivider Class="my-2" />
|
||||
|
||||
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">Home</MudNavLink>
|
||||
|
||||
<MudNavLink Href="/contacts" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.List">Contact List</MudNavLink>
|
||||
<MudDivider Class="my-2" />
|
||||
|
||||
</MudNavMenu>
|
||||
@@ -0,0 +1,68 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.21" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.21" PrivateAssets="all" />
|
||||
<PackageReference Include="MudBlazor" Version="8.13.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Sufi.Demo.PeopleDirectory.Application\Sufi.Demo.PeopleDirectory.Application.csproj" />
|
||||
<ProjectReference Include="..\..\..\Sufi.Demo.PeopleDirectory.Shared\Sufi.Demo.PeopleDirectory.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Pages\Auth\" />
|
||||
</ItemGroup>
|
||||
|
||||
<UsingTask TaskName="ComputeHtmlTicks" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||
<ParameterGroup>
|
||||
<Ticks ParameterType="System.String" Output="true" />
|
||||
</ParameterGroup>
|
||||
<Task>
|
||||
<Using Namespace="System" />
|
||||
<Code Type="Fragment" Language="cs">
|
||||
<![CDATA[
|
||||
Ticks = DateTime.UtcNow.Ticks.ToString();
|
||||
]]>
|
||||
</Code>
|
||||
</Task>
|
||||
</UsingTask>
|
||||
|
||||
<UsingTask TaskName="ReplaceTokenInFile" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||
<ParameterGroup>
|
||||
<TemplateFile ParameterType="System.String" Required="true" />
|
||||
<OutputFile ParameterType="System.String" Required="true" />
|
||||
<Token ParameterType="System.String" Required="true" />
|
||||
<Replacement ParameterType="System.String" Required="true" />
|
||||
</ParameterGroup>
|
||||
<Task>
|
||||
<Using Namespace="System" />
|
||||
<Using Namespace="System.IO" />
|
||||
<Code Type="Fragment" Language="cs">
|
||||
<![CDATA[
|
||||
var content = File.ReadAllText(TemplateFile);
|
||||
content = content.Replace(Token, Replacement);
|
||||
File.WriteAllText(OutputFile, content);
|
||||
]]>
|
||||
</Code>
|
||||
</Task>
|
||||
</UsingTask>
|
||||
|
||||
<Target Name="InjectHtmlTicks" BeforeTargets="Build;Publish">
|
||||
<ComputeHtmlTicks>
|
||||
<Output TaskParameter="Ticks" PropertyName="HtmlTicks" />
|
||||
</ComputeHtmlTicks>
|
||||
|
||||
<Message Text="Computed Ticks: $(HtmlTicks)" Importance="Normal" />
|
||||
|
||||
<!-- write wwwroot/index.html from template -->
|
||||
<ReplaceTokenInFile TemplateFile="$(MSBuildProjectDirectory)\wwwroot\index.template.html" OutputFile="$(MSBuildProjectDirectory)\wwwroot\index.html" Token="__HTML_TICKS__" Replacement="$(HtmlTicks)" />
|
||||
</Target>
|
||||
</Project>
|
||||
17
ui/Sufi.Demo.PeopleDirectory.UI/Client/_Imports.razor
Normal file
17
ui/Sufi.Demo.PeopleDirectory.UI/Client/_Imports.razor
Normal file
@@ -0,0 +1,17 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Http
|
||||
@using Microsoft.JSInterop
|
||||
@using Sufi.Demo.PeopleDirectory.UI.Client
|
||||
@using Sufi.Demo.PeopleDirectory.UI.Client.Shared
|
||||
@using MudBlazor;
|
||||
@using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Commands
|
||||
@using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetAll
|
||||
@using Sufi.Demo.PeopleDirectory.Shared.Wrapper
|
||||
@using Sufi.Demo.PeopleDirectory.UI.Client.Pages.Dialogs
|
||||
@using Sufi.Demo.PeopleDirectory.Shared.Extensions
|
||||
@using Sufi.Demo.PeopleDirectory.UI.Client.Services
|
||||
62
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/css/app.css
Normal file
62
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/css/app.css
Normal file
@@ -0,0 +1,62 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #0071c1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
BIN
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/favicon.ico
Normal file
BIN
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
BIN
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/icon-192.png
Normal file
BIN
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/icon-192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
34
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/index.html
Normal file
34
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/index.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Sufi Demo App</title>
|
||||
<base href="/" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css?v=639044765520078561" rel="stylesheet" />
|
||||
<link href="css/app.css?v=639044765520078561" rel="stylesheet" />
|
||||
<link href="Sufi.Demo.PeopleDirectory.UI.Client.styles.css?v=639044765520078561" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">Loading...</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js?v=639044765520078561"></script>
|
||||
<script src="js/boomerang.js"></script>
|
||||
<script src="js/boomerang-interop.js"></script>
|
||||
<script>
|
||||
BOOMR.init({
|
||||
beacon_url: "api/v1/beacon/"
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Sufi Demo App</title>
|
||||
<base href="/" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css?v=__HTML_TICKS__" rel="stylesheet" />
|
||||
<link href="css/app.css?v=__HTML_TICKS__" rel="stylesheet" />
|
||||
<link href="Sufi.Demo.PeopleDirectory.UI.Client.styles.css?v=__HTML_TICKS__" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">Loading...</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js?v=__HTML_TICKS__"></script>
|
||||
<script src="js/boomerang.js"></script>
|
||||
<script src="js/boomerang-interop.js"></script>
|
||||
<script>
|
||||
BOOMR.init({
|
||||
beacon_url: "api/v1/beacon/"
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
window.boomerangHelper = {
|
||||
init: function (config) {
|
||||
if (window.BOOMR && window.BOOMR.init) {
|
||||
BOOMR.init(config);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
addVar: function (key, value) {
|
||||
if (window.BOOMR) {
|
||||
BOOMR.addVar(key, value);
|
||||
}
|
||||
},
|
||||
|
||||
sendBeacon: function () {
|
||||
if (window.BOOMR) {
|
||||
BOOMR.sendBeacon();
|
||||
}
|
||||
}
|
||||
};
|
||||
5360
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/js/boomerang.js
Normal file
5360
ui/Sufi.Demo.PeopleDirectory.UI/Client/wwwroot/js/boomerang.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "7.0.10",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent the base controller class.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
[ApiController]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
public abstract class BaseApiController<T> : ControllerBase
|
||||
{
|
||||
private IMediator? _mediatorInstance;
|
||||
private ILogger<T>? _loggerInstance;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the mediator for requests/responses.
|
||||
/// </summary>
|
||||
protected IMediator Mediator => _mediatorInstance ??= HttpContext.RequestServices.GetRequiredService<IMediator>();
|
||||
/// <summary>
|
||||
/// Gets the logger for current controller.
|
||||
/// </summary>
|
||||
protected ILogger<T> Logger => _loggerInstance ??= HttpContext.RequestServices.GetRequiredService<ILogger<T>>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net.Mime;
|
||||
using System.Text.Json;
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Controllers.v1
|
||||
{
|
||||
[ApiVersion(1.0)]
|
||||
public class BeaconController : BaseApiController<BeaconController>
|
||||
{
|
||||
[HttpGet]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var beaconData = new Dictionary<string, string>();
|
||||
|
||||
if (Request.Method == "GET")
|
||||
{
|
||||
foreach (var param in Request.Query)
|
||||
{
|
||||
beaconData[param.Key] = param.Value.ToString();
|
||||
}
|
||||
}
|
||||
else if (Request.Method == "POST")
|
||||
{
|
||||
var contentType = Request.ContentType?.ToLower();
|
||||
|
||||
if (contentType?.Contains(MediaTypeNames.Application.Json) == true)
|
||||
{
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
var jsonData = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(body)!;
|
||||
|
||||
foreach (var kvp in jsonData)
|
||||
{
|
||||
beaconData[kvp.Key] = kvp.Value.ToString();
|
||||
}
|
||||
}
|
||||
else if (contentType?.Contains(MediaTypeNames.Application.FormUrlEncoded) == true)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
foreach (var kvp in form)
|
||||
{
|
||||
beaconData[kvp.Key] = kvp.Value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Commands;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetAll;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Features.Contacts.Queries.GetById;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Controllers.v1
|
||||
{
|
||||
/// <summary>
|
||||
/// A controller for manipulating contact data.
|
||||
/// </summary>
|
||||
[ApiVersion(1.0)]
|
||||
public class ContactsController : BaseApiController<ContactsController>
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all contacts.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetAll() => Ok(await Mediator.Send(new GetAllContactsQuery()));
|
||||
|
||||
/// <summary>
|
||||
/// Get a contact by id.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> Get(int id) => Ok(await Mediator.Send(new GetContactByIdQuery { Id = id }));
|
||||
|
||||
/// <summary>
|
||||
/// Create/Update a contact.
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Post(AddEditContactCommand request) => Ok(await Mediator.Send(request));
|
||||
|
||||
/// <summary>
|
||||
/// Delete a contact.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[HttpDelete("{id}")]
|
||||
public async Task<IActionResult> Delete([Required] int id)
|
||||
{
|
||||
var command = new DeleteContactCommand { Id = id };
|
||||
return Ok(await Mediator.Send(command));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Controllers.v1
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides API endpoints for infrastructure-related operations.
|
||||
/// </summary>
|
||||
/// <remarks>This controller is part of the infrastructure layer and includes endpoints for monitoring and diagnostics.</remarks>
|
||||
[ApiVersion(1.0)]
|
||||
public class InfraController(
|
||||
ILogger<InfraController> logger
|
||||
) : BaseApiController<InfraController>
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulates a ping to check if the server is responsive.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Route("ping")]
|
||||
[HttpGet]
|
||||
public IActionResult Ping()
|
||||
{
|
||||
logger.LogInformation("InfraController.Ping method called.");
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Controllers.v2
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides API endpoints for infrastructure-related operations.
|
||||
/// </summary>
|
||||
/// <remarks>This controller is part of the infrastructure layer and includes endpoints for monitoring and diagnostics.</remarks>
|
||||
[ApiVersion(2.0)]
|
||||
public class InfraController : BaseApiController<InfraController>
|
||||
{
|
||||
/// <summary>
|
||||
/// Simulates a ping to check if the server is responsive.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Route("ping")]
|
||||
[HttpGet]
|
||||
public IActionResult Ping() => Ok("Server is OK");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension class.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
internal static void RegisterSwagger(this IServiceCollection services)
|
||||
{
|
||||
services.ConfigureOptions<ConfigureSwaggerGenOptions>();
|
||||
services.AddSwaggerGen(c =>
|
||||
{
|
||||
//TODO - Lowercase Swagger Documents
|
||||
//c.DocumentFilter<LowercaseDocumentFilter>();
|
||||
//Refer - https://gist.github.com/rafalkasa/01d5e3b265e5aa075678e0adfd54e23f
|
||||
|
||||
// include all project's xml comments
|
||||
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
if (!assembly.IsDynamic)
|
||||
{
|
||||
var xmlFile = $"{assembly.GetName().Name}.xml";
|
||||
var xmlPath = Path.Combine(baseDirectory, xmlFile);
|
||||
if (File.Exists(xmlPath))
|
||||
{
|
||||
c.IncludeXmlComments(xmlPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.OperationFilter<SwaggerDefaultValues>();
|
||||
})
|
||||
.AddApiVersioning(config =>
|
||||
{
|
||||
config.DefaultApiVersion = new ApiVersion(1, 0);
|
||||
config.AssumeDefaultVersionWhenUnspecified = true;
|
||||
config.ApiVersionReader = new UrlSegmentApiVersionReader();
|
||||
})
|
||||
.AddApiExplorer(options =>
|
||||
{
|
||||
options.GroupNameFormat = "'v'VVV";
|
||||
options.SubstituteApiVersionInUrl = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal class ConfigureSwaggerGenOptions(IApiVersionDescriptionProvider provider) : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
public void Configure(SwaggerGenOptions options)
|
||||
{
|
||||
foreach (var description in provider.ApiVersionDescriptions)
|
||||
{
|
||||
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
|
||||
}
|
||||
}
|
||||
|
||||
private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
|
||||
{
|
||||
var text = new StringBuilder("This application is for demo purposes by Sufi only.");
|
||||
var info = new OpenApiInfo
|
||||
{
|
||||
Title = "Sufi.Demo.App",
|
||||
Version = description.ApiVersion.ToString(),
|
||||
License = new OpenApiLicense
|
||||
{
|
||||
Name = "MIT License",
|
||||
Url = new Uri("https://opensource.org/licenses/MIT")
|
||||
}
|
||||
};
|
||||
|
||||
if (description.IsDeprecated)
|
||||
{
|
||||
text.Append("This Api version has been deprecated.");
|
||||
}
|
||||
|
||||
if (description.SunsetPolicy is SunsetPolicy policy && policy.Date is DateTimeOffset when)
|
||||
{
|
||||
text.Append(" The Api will be sunset on ")
|
||||
.Append(when.Date.ToShortDateString())
|
||||
.Append('.');
|
||||
}
|
||||
|
||||
info.Description = text.ToString();
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
internal class SwaggerDefaultValues : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
var apiDescription = context.ApiDescription;
|
||||
|
||||
operation.Deprecated |= apiDescription.IsDeprecated();
|
||||
|
||||
foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
|
||||
{
|
||||
var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
|
||||
var response = operation.Responses[responseKey];
|
||||
|
||||
foreach (var contentType in response.Content.Keys)
|
||||
{
|
||||
if (!responseType.ApiResponseFormats.Any(x => x.MediaType == contentType))
|
||||
{
|
||||
response.Content.Remove(contentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (operation.Parameters == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var parameter in operation.Parameters)
|
||||
{
|
||||
var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
|
||||
|
||||
parameter.Description ??= description.ModelMetadata.Description;
|
||||
|
||||
if (parameter.Schema.Default == null &&
|
||||
description.DefaultValue != null &&
|
||||
description.DefaultValue is not DBNull &&
|
||||
description.ModelMetadata is ModelMetadata modelMetadata)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(description.DefaultValue, modelMetadata.ModelType);
|
||||
parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
|
||||
}
|
||||
|
||||
parameter.Required |= description.IsRequired;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents configuration options for rate limiting functionality.
|
||||
/// </summary>
|
||||
/// <remarks>This type is used to define the parameters for controlling the rate at which operations are
|
||||
/// allowed. It specifies the maximum number of permits and the time window during which the permits are
|
||||
/// valid.</remarks>
|
||||
public record RateLimitOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the maximum number of permits that can be issued.
|
||||
/// </summary>
|
||||
public int PermitLimit { get; init; } = 100;
|
||||
/// <summary>
|
||||
/// Gets the time interval that defines the window for rate-limiting operations (in seconds).
|
||||
/// </summary>
|
||||
public int Window { get; init; } = 60;
|
||||
}
|
||||
}
|
||||
42
ui/Sufi.Demo.PeopleDirectory.UI/Server/Pages/Error.cshtml
Normal file
42
ui/Sufi.Demo.PeopleDirectory.UI/Server/Pages/Error.cshtml
Normal file
@@ -0,0 +1,42 @@
|
||||
@page
|
||||
@model Sufi.Demo.PeopleDirectory.UI.Server.Pages.ErrorModel
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Error</title>
|
||||
<link href="~/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="~/css/app.css" rel="stylesheet" asp-append-version="true" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="main">
|
||||
<div class="content px-4">
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (Model.ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
29
ui/Sufi.Demo.PeopleDirectory.UI/Server/Pages/Error.cshtml.cs
Normal file
29
ui/Sufi.Demo.PeopleDirectory.UI/Server/Pages/Error.cshtml.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using System.Diagnostics;
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server.Pages
|
||||
{
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public class ErrorModel : PageModel
|
||||
{
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
private readonly ILogger<ErrorModel> _logger;
|
||||
|
||||
public ErrorModel(ILogger<ErrorModel> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void OnGet()
|
||||
{
|
||||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
ui/Sufi.Demo.PeopleDirectory.UI/Server/Program.cs
Normal file
22
ui/Sufi.Demo.PeopleDirectory.UI/Server/Program.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Serilog;
|
||||
using Sufi.Demo.PeopleDirectory.UI.Server;
|
||||
|
||||
try
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var app = builder.ConfigureServices()
|
||||
.ConfigurePipeline();
|
||||
|
||||
// Ensure all pending migrations are applied.
|
||||
await app.EnsureDatabaseMigrationAsync();
|
||||
|
||||
app.Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Application terminated unexpectedly");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:50212",
|
||||
"sslPort": 44358
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"Sufi.Demo.PeopleDirectory.UI.Server": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"applicationUrl": "https://localhost:7024;http://localhost:5243",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
129
ui/Sufi.Demo.PeopleDirectory.UI/Server/StartupExtensions.cs
Normal file
129
ui/Sufi.Demo.PeopleDirectory.UI/Server/StartupExtensions.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
using Sufi.Demo.PeopleDirectory.Application.Extensions;
|
||||
using Sufi.Demo.PeopleDirectory.Infrastructure;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Contexts;
|
||||
using Sufi.Demo.PeopleDirectory.Persistence.Extensions;
|
||||
using Sufi.Demo.PeopleDirectory.UI.Server.Extensions;
|
||||
using Sufi.Demo.PeopleDirectory.UI.Server.Options;
|
||||
using System.Threading.RateLimiting;
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
namespace Sufi.Demo.PeopleDirectory.UI.Server
|
||||
{
|
||||
public static class StartupExtensions
|
||||
{
|
||||
public static WebApplication ConfigureServices(this WebApplicationBuilder builder)
|
||||
{
|
||||
var configuration = builder.Configuration;
|
||||
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(configuration)
|
||||
.CreateLogger();
|
||||
builder.Host.UseSerilog();
|
||||
|
||||
var rateLimitOptions = configuration.GetSection("RateLimit").Get<RateLimitOptions>()!;
|
||||
var services = builder.Services;
|
||||
var licenseKey = configuration.GetValue<string>("LuckyPennyLicenseKey") ?? string.Empty;
|
||||
|
||||
// Add services to the container.
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddApplicationLayer(licenseKey)
|
||||
.AddInfrastructureServices()
|
||||
.AddPersistenceServices(configuration);
|
||||
|
||||
services.AddControllersWithViews();
|
||||
services.AddRazorPages();
|
||||
services.AddHealthChecks();
|
||||
|
||||
services.AddRateLimiter(options =>
|
||||
{
|
||||
// Apply for all requests.
|
||||
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
|
||||
RateLimitPartition.GetFixedWindowLimiter(
|
||||
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||
factory: partition => new FixedWindowRateLimiterOptions
|
||||
{
|
||||
AutoReplenishment = true,
|
||||
PermitLimit = rateLimitOptions.PermitLimit,
|
||||
Window = TimeSpan.FromSeconds(rateLimitOptions.Window)
|
||||
}));
|
||||
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
});
|
||||
|
||||
// Register Swagger services.
|
||||
services.AddEndpointsApiExplorer();
|
||||
services.RegisterSwagger();
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
|
||||
public static WebApplication ConfigurePipeline(this WebApplication app)
|
||||
{
|
||||
app.UseSerilogRequestLogging(options =>
|
||||
{
|
||||
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
|
||||
{
|
||||
diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value);
|
||||
diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent);
|
||||
diagnosticContext.Set("ClientIP", httpContext.Connection.RemoteIpAddress?.ToString() ?? "");
|
||||
};
|
||||
});
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseWebAssemblyDebugging();
|
||||
app.ConfigureSwagger();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseBlazorFrameworkFiles();
|
||||
app.UseStaticFiles();
|
||||
|
||||
app.UseRouting();
|
||||
app.UseRateLimiter();
|
||||
|
||||
app.MapHealthChecks("/health");
|
||||
app.MapRazorPages();
|
||||
app.MapControllers();
|
||||
app.MapFallbackToFile("index.html");
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
public static void ConfigureSwagger(this IApplicationBuilder app)
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(options =>
|
||||
{
|
||||
options.RoutePrefix = "swagger";
|
||||
options.DisplayRequestDuration();
|
||||
|
||||
foreach (var desc in ((IEndpointRouteBuilder)app).DescribeApiVersions())
|
||||
{
|
||||
options.SwaggerEndpoint($"{desc.GroupName}/swagger.json",
|
||||
desc.GroupName.ToUpperInvariant());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task<IApplicationBuilder> EnsureDatabaseMigrationAsync(this IApplicationBuilder app)
|
||||
{
|
||||
using (var scope = app.ApplicationServices.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
await dbContext.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>19cb1e83-775a-41dd-a289-4d267a75a265</UserSecretsId>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.21" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.21" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.21">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Sufi.Demo.PeopleDirectory.Persistence\Sufi.Demo.PeopleDirectory.Persistence.csproj" />
|
||||
<ProjectReference Include="..\..\..\Sufi.Demo.PeopleDirectory.Infrastructure\Sufi.Demo.PeopleDirectory.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\..\Sufi.Demo.PeopleDirectory.Persistence\Sufi.Demo.PeopleDirectory.Persistence.csproj" />
|
||||
<ProjectReference Include="..\Client\Sufi.Demo.PeopleDirectory.UI.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnectionString": "Server=localhost:5432;Database=demo-contact;User Id=postgres;Password=Abcd@1234;"
|
||||
},
|
||||
"Serilog": {
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <{Application}>{NewLine}{Exception}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <{Application}>{NewLine}{Exception}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "Seq",
|
||||
"Args": {
|
||||
"serverUrl": "",
|
||||
"apiKey": "",
|
||||
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <{Application}>{NewLine}{Exception}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
37
ui/Sufi.Demo.PeopleDirectory.UI/Server/appsettings.json
Normal file
37
ui/Sufi.Demo.PeopleDirectory.UI/Server/appsettings.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnectionString": ""
|
||||
},
|
||||
"Serilog": {
|
||||
"Using": [
|
||||
"Serilog.Sinks.Console",
|
||||
"Serilog.Sinks.Seq"
|
||||
],
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"Microsoft.EntityFrameworkCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Enrich": [
|
||||
"FromLogContext",
|
||||
"WithProperty:Application"
|
||||
],
|
||||
"Properties": {
|
||||
"Application": "Sufi.Demo.PeopleDirectory"
|
||||
}
|
||||
},
|
||||
"RateLimit": {
|
||||
"PermitLimit": 100,
|
||||
"Window": 60
|
||||
},
|
||||
"LuckyPennyLicenseKey": ""
|
||||
}
|
||||
Reference in New Issue
Block a user