Skip to content

Commit

Permalink
Basic Blog Post Functionality (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
wbaldoumas committed Nov 30, 2021
1 parent 0bcde6e commit 922078e
Show file tree
Hide file tree
Showing 37 changed files with 935 additions and 110 deletions.
1 change: 1 addition & 0 deletions src/Coding.Blog/Client/Coding.Blog.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.40.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.0" PrivateAssets="all" />
<PackageReference Include="TaskTupleAwaiter" Version="2.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
38 changes: 34 additions & 4 deletions src/Coding.Blog/Client/Pages/Blog.razor
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
@page "/blog"
@using Coding.Blog.Engine
@using Coding.Blog.Engine.Extensions

<h3>Blog</h3>

<p>Under construction</p>
<div class="container mb-3">
@if (!Posts.Any())
{
<p>
<strong>Loading...</strong>
</p>
}
else
{
<div class="row row-cols-sm-1 row-cols-md-2 row-cols-lg-3 g-3">
@foreach (var (slug, post) in Posts)
{
<div class="col">
<div class="card">
<a href="@($"post/{slug}")">
<img src=@post.Hero.ImgixUrl class="card-img-top" alt="hero"/>
</a>
<div class="card-body">
<h5 class="card-title">@post.Title</h5>
<h6 class="card-subtitle text-muted">@post.DatePublished.ToShortDateString()</h6>
<a href="@($"post/{slug}")" class="card-link">Read More</a>
</div>
</div>
</div>
}
</div>
}
</div>

@code {

}
[CascadingParameter(Name = "Posts")]
private IDictionary<string, Post> Posts { get; set; } = new Dictionary<string, Post>();

}
97 changes: 97 additions & 0 deletions src/Coding.Blog/Client/Pages/PostDetails.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
@page "/post/{Slug}"
@inject NavigationManager _navigationManager
@inject IResilientClient<Post> _postsClient;
@inject IJSRuntime _js;
@using Coding.Blog.Engine
@using Coding.Blog.Engine.Clients
@using Coding.Blog.Engine.Extensions

@if (_selectedPost is null)
{
<p>
<strong>Loading...</strong>
</p>
}
else
{
<div class="container">
<div class="row justify-content-center">
<div class="col col-auto">
<h1>@_selectedPost!.Title</h1>
</div>
</div>
<div class="row justify-content-center">
<div class="col col-auto">
<p class="text-muted">@_selectedPost!.DatePublished.ToShortDateString()</p>
</div>
</div>
</div>
<div class="container overflow-hidden mb-3">
<div class="row justify-content-center">
<div class="col col-auto">
<img class="rounded mx-auto d-block img-fluid" src="@_selectedPost!.Hero.ImgixUrl" alt="hero">
</div>
</div>
<div class="row">
<div class="col">
<div>@(new MarkupString(_selectedPost!.Content))</div>
</div>
</div>
<div class="row justify-content-between">
<div class="col col-auto">
@if (_selectedPost.Next is not null)
{
<h6 class="text-muted">Next</h6>
<a href="@($"post/{_selectedPost.Next.Slug}")">
<h5>@_selectedPost.Next.Title</h5>
</a>
}
</div >
<div class="col col-auto">
@if (_selectedPost.Previous is not null)
{
<h6 class="text-muted">Previous</h6>
<a href="@($"post/{_selectedPost.Previous.Slug}")">
<h5>@_selectedPost.Previous.Title</h5>
</a>
}
</div>
</div>
<div class="row justify-content-center">
<div class="col col-auto">
<button class="btn btn-secondary btn-lg" @onclick="NavigateToMain">Back</button>
</div>
</div>
</div>
}

@code {

[Parameter]
public string Slug { get; set; } = string.Empty;

[CascadingParameter(Name = "Posts")]
private IDictionary<string, Post> Posts { get; set; } = new Dictionary<string, Post>();

private Post? _selectedPost;

protected override async Task OnInitializedAsync() => await Refresh();

protected override async Task OnParametersSetAsync() => await Refresh();

private async Task Refresh()
{
await _js.InvokeVoidAsync("resetScrollPosition");

if (!Posts.Any())
{
var posts = await _postsClient.GetAsync();

Posts = posts.ToDictionary(post => post.Slug, post => post);
}

Posts.TryGetValue(Slug, out _selectedPost);
}

private void NavigateToMain() => _navigationManager.NavigateTo("blog");
}
29 changes: 15 additions & 14 deletions src/Coding.Blog/Client/Pages/Reading.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@page "/reading"
@using Coding.Blog.Engine
@using Coding.Blog.Engine.Extensions

<h1>Reading Recommendations</h1>

Expand All @@ -8,19 +9,19 @@
Below are some reading recommendations based on books that I've read and enjoyed. Most are programming-language and technology agnostic and instead focus on building up the fundamentals needed for a successful career as a developer.
</p>
<p>
<em>Last updated: @(Books.Any() ? Books.Max(book => book.DatePublished)!.ToDateTime().ToShortDateString() : "Loading...")</em>
<em>Last updated: @(Books.Any() ? Books.Max(book => book.DatePublished)!.ToShortDateString() : "Loading...")</em>
</p>
</div>

@if (!Books.Any())
{
<p>
<em>Loading...</em>
</p>
}
else
{
<div class="container">
<div class="container">
@if (!Books.Any())
{
<p>
<strong>Loading...</strong>
</p>
}
else
{
@foreach (var book in Books)
{
<hr/>
Expand All @@ -42,12 +43,12 @@ else
</div>
</div>
}
</div>
}
}
</div>

@code {

[CascadingParameter(Name="Books")]
private IEnumerable<Book> Books {get; set;} = new List<Book>();
[CascadingParameter(Name = "Books")]
private IEnumerable<Book> Books { get; set; } = new List<Book>();

}
70 changes: 50 additions & 20 deletions src/Coding.Blog/Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,30 @@ static void RegisterKeyedConfiguration<T>(WebAssemblyHostBuilder webAssemblyHost
RegisterKeyedConfiguration<GrpcResilienceConfiguration>(builder);
RegisterKeyedConfiguration<ResilienceConfiguration>(builder);

builder.Services.AddSingleton(serviceProvider =>
{
var grpcResilienceConfiguration = serviceProvider.GetRequiredService<GrpcResilienceConfiguration>();
return new ServiceConfig
{
MethodConfigs =
{
new MethodConfig
{
Names = { MethodName.Default },
RetryPolicy = new RetryPolicy
{
MaxAttempts = grpcResilienceConfiguration.MaxAttempts,
InitialBackoff = TimeSpan.FromMilliseconds(grpcResilienceConfiguration.InitialBackoffMilliseconds),
MaxBackoff = TimeSpan.FromMilliseconds(grpcResilienceConfiguration.MaxBackoffMilliseconds),
BackoffMultiplier = grpcResilienceConfiguration.BackoffMultiplier,
RetryableStatusCodes = { StatusCode.Unavailable }
}
}
}
};
});

builder.Services
.AddGrpcClient<Books.BooksClient>((serviceProvider, grpcClientFactoryOptions) =>
{
Expand All @@ -35,29 +59,21 @@ static void RegisterKeyedConfiguration<T>(WebAssemblyHostBuilder webAssemblyHost
.ConfigurePrimaryHttpMessageHandler(() => new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()))
.ConfigureChannel((serviceProvider, grpcChannelOptions) =>
{
var grpcResilienceConfiguration = serviceProvider.GetRequiredService<GrpcResilienceConfiguration>();
grpcChannelOptions.ServiceConfig = serviceProvider.GetRequiredService<ServiceConfig>();
});

grpcChannelOptions.ServiceConfig = new ServiceConfig
{
MethodConfigs =
{
new MethodConfig
{
Names = { MethodName.Default },
RetryPolicy = new RetryPolicy
{
MaxAttempts = grpcResilienceConfiguration.MaxAttempts,
InitialBackoff = TimeSpan.FromMilliseconds(grpcResilienceConfiguration.InitialBackoffMilliseconds),
MaxBackoff = TimeSpan.FromMilliseconds(grpcResilienceConfiguration.MaxBackoffMilliseconds),
BackoffMultiplier = grpcResilienceConfiguration.BackoffMultiplier,
RetryableStatusCodes = { StatusCode.Unavailable }
}
}
}
};
builder.Services
.AddGrpcClient<Posts.PostsClient>((serviceProvider, grpcClientFactoryOptions) =>
{
grpcClientFactoryOptions.Address = new Uri(serviceProvider.GetRequiredService<NavigationManager>().BaseUri);
})
.ConfigurePrimaryHttpMessageHandler(() => new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler()))
.ConfigureChannel((serviceProvider, grpcChannelOptions) =>
{
grpcChannelOptions.ServiceConfig = serviceProvider.GetRequiredService<ServiceConfig>();
});

builder.Services.AddSingleton<IResilientBooksClient, ResilientBooksClient>(serviceProvider =>
builder.Services.AddSingleton<IResilientClient<Book>, ResilientBooksClient>(serviceProvider =>
{
var configuration = serviceProvider.GetRequiredService<ResilienceConfiguration>();
var booksClient = serviceProvider.GetRequiredService<Books.BooksClient>();
Expand All @@ -71,4 +87,18 @@ static void RegisterKeyedConfiguration<T>(WebAssemblyHostBuilder webAssemblyHost
return new ResilientBooksClient(booksClient, logger, resiliencePolicy);
});

builder.Services.AddSingleton<IResilientClient<Post>, ResilientPostsClient>(serviceProvider =>
{
var configuration = serviceProvider.GetRequiredService<ResilienceConfiguration>();
var booksClient = serviceProvider.GetRequiredService<Posts.PostsClient>();
var logger = serviceProvider.GetRequiredService<ILogger<Post>>();
var resiliencePolicy = ResiliencePolicyBuilder.Build<IEnumerable<Post>>(
TimeSpan.FromMilliseconds(configuration.MedianFirstRetryDelayMilliseconds),
configuration.RetryCount,
TimeSpan.FromMilliseconds(configuration.TimeToLiveMilliseconds)
);
return new ResilientPostsClient(booksClient, logger, resiliencePolicy);
});

await builder.Build().RunAsync();
24 changes: 19 additions & 5 deletions src/Coding.Blog/Client/Shared/MainLayout.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@using Coding.Blog.Engine
@using Coding.Blog.Engine.Clients
@inject IResilientBooksClient _booksClient;
@inject IResilientClient<Book> _booksClient;
@inject IResilientClient<Post> _postsClient;
@inherits LayoutComponentBase

<div class="page">
Expand All @@ -9,15 +10,28 @@
</div>
<main>
<CascadingValue Name="Books" Value="@_books">
<article class="content px-4">
@Body
</article>
<CascadingValue Name="Posts" Value="@_posts">
<article class="content px-4">
@Body
</article>
</CascadingValue>
</CascadingValue>
</main>
</div>

@code {
private IEnumerable<Book> _books = new List<Book>();
private IDictionary<string, Post> _posts = new Dictionary<string, Post>();

protected override async Task OnInitializedAsync()
{
var (books, posts) = await (_booksClient.GetAsync(), _postsClient.GetAsync());

_books = new List<Book>(books);

_posts = new Dictionary<string, Post>(
posts.ToDictionary(post => post.Slug, post => post)
);
}

protected override async Task OnInitializedAsync() => _books = await _booksClient.GetAsync();
}
7 changes: 7 additions & 0 deletions src/Coding.Blog/Client/wwwroot/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Grpc": "Information",
"Microsoft.AspNetCore": "Information"
}
},
"Resilience": {
"RetryCount": 5,
"TimeToLiveMilliseconds": 10000
Expand Down
4 changes: 2 additions & 2 deletions src/Coding.Blog/Client/wwwroot/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Default": "Warning",
"Grpc": "Warning",
"Microsoft.AspNetCore": "Warning"
}
Expand All @@ -17,4 +17,4 @@
"RetryCount": 5,
"TimeToLiveMilliseconds": 86400000
}
}
}
4 changes: 0 additions & 4 deletions src/Coding.Blog/Client/wwwroot/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ h1:focus {
outline: none;
}

a, .btn-link {
color: #0071c1;
}

.btn-primary {
color: #fff;
background-color: #1b6ec2;
Expand Down

Large diffs are not rendered by default.

This file was deleted.

0 comments on commit 922078e

Please sign in to comment.