Adopt b2b.next's client patterns: services, page state, feedback
Ported from the owner's b2b.next and adapted from MudBlazor to the daisyUI
components:
- LocalizableComponentBase provides CascadingState, Navigation, Toast,
IsLoading/IsDefaultDisabled, WhenOnline/StartOnlinePolling, and after-render
jobs that actually run.
- CascadingState starts with initialised state and polls the online status
with a PeriodicTimer. It reports errors through ILoggingService plus a toast,
and disposes what it holds.
- New pieces:
- ToastService + ToastHost, the snackbar, with AddResponseError/Success/
Warning/Exception in SUtility;
- ErrorViewer, which shows the validation messages plus AdditionalError(s);
- LoggingService, which writes to the console and the IndexedDB client log.
- HttpService is rebuilt and registered:
- auth levels Get/Post/Delete, *Anon and *TotallyAnon;
- retry with backoff;
- classified failures (FailureCodes) instead of exceptions;
- HTTP 410 on cancel;
- the "default" client.
ReadWebResult and DefaultReadFromJsonAsync read its responses.
- ClientJsonContext is source-generated, and SUtility.DefaultSerializer and
Blazored.LocalStorage use it.
- WebResult gains a C# 14 IsInvalid extension property.
The code follows the imported rules:
- Every service and async helper takes a CancellationToken last.
- Primary constructors replace constructor-plus-fields.
- MessagesService and IStorage return Task<WebResult> / Task<List<T>>.
MessagesService is still mock, building its results in memory; the seed
feed and thread moved to Faker.
- Index and ExpandMessage follow the page pattern: an offline guard with
WhenOnline, LoadData, and early returns with toasts on the results they used
to ignore. ExpandMessage reloads on route change and shows "not found"
instead of a null message.
- Model and state collections are [], and the fake identity defaults moved
out of the models.
- Components:
- no ?. chains over initialised state;
- [Inject] instead of @inject;
- IsDefaultDisabled on the actions;
- autocomplete on every text input;
- Localizer[key, args] instead of string.Format;
- the localizer no longer leaks its inner _pLocalizer.
- Settings → General gains a language picker (English/Italiano), and
SetDefaultCulture limits the culture to those two.
Bugs fixed along the way:
- A compose with untouched (null) content passed the empty check.
- File errors were joined with <br/> and rendered as markup.
- ClientLogs.ErrorMessage() threw on warnings.
- CoalescingStringLocalizer[key, args] showed a raw "{0}" for missing keys.
- AppStatusService exposed a call to a JS function that doesn't exist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
ca060f25fb
commit
95428430c0
55 files changed
+1844
-1337
No files matched your search
@@ -11,31 +11,31 @@
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@foreach (var color in Enum.GetValues<DColor>())
|
||||
{
|
||||
<DInput @bind-Value="text" Color="color" placeholder="@color.ToString()" class="w-36" />
|
||||
<DInput autocomplete="off" @bind-Value="text" Color="color" placeholder="@color.ToString()" class="w-36" />
|
||||
}
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<DInput @bind-Value="text" Size="DSize.Xs" placeholder="Xs" class="w-28" />
|
||||
<DInput @bind-Value="text" Size="DSize.Sm" placeholder="Sm" class="w-28" />
|
||||
<DInput @bind-Value="text" placeholder="Md" class="w-28" />
|
||||
<DInput @bind-Value="text" Size="DSize.Lg" placeholder="Lg" class="w-28" />
|
||||
<DInput @bind-Value="text" Size="DSize.Xl" placeholder="Xl" class="w-28" />
|
||||
<DInput autocomplete="off" @bind-Value="text" Size="DSize.Xs" placeholder="Xs" class="w-28" />
|
||||
<DInput autocomplete="off" @bind-Value="text" Size="DSize.Sm" placeholder="Sm" class="w-28" />
|
||||
<DInput autocomplete="off" @bind-Value="text" placeholder="Md" class="w-28" />
|
||||
<DInput autocomplete="off" @bind-Value="text" Size="DSize.Lg" placeholder="Lg" class="w-28" />
|
||||
<DInput autocomplete="off" @bind-Value="text" Size="DSize.Xl" placeholder="Xl" class="w-28" />
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<DInput @bind-Value="text" Ghost="true" placeholder="Ghost" />
|
||||
<DInput @bind-Value="text" disabled placeholder="Disabled" />
|
||||
<DInput @bind-Value="search" Immediate="true" placeholder="Search">
|
||||
<DInput autocomplete="off" @bind-Value="text" Ghost="true" placeholder="Ghost" />
|
||||
<DInput autocomplete="off" @bind-Value="text" disabled placeholder="Disabled" />
|
||||
<DInput autocomplete="off" @bind-Value="search" Immediate="true" placeholder="Search">
|
||||
<Prefix><i class="ion-md-search opacity-50"></i></Prefix>
|
||||
<Suffix><kbd class="kbd kbd-sm">⌘</kbd><kbd class="kbd kbd-sm">K</kbd></Suffix>
|
||||
</DInput>
|
||||
<DInput @bind-Value="site" Color="DColor.Primary">
|
||||
<DInput autocomplete="off" @bind-Value="site" Color="DColor.Primary">
|
||||
<Prefix><DLabel>https://</DLabel></Prefix>
|
||||
<Suffix><DLabel>.com</DLabel></Suffix>
|
||||
</DInput>
|
||||
</div>
|
||||
<p class="text-sm opacity-60">Immediate search: "@search", text: "@text"</p>
|
||||
<div class="flex flex-col">
|
||||
<DInput @bind-Value="model.Name" Validator="true" placeholder="Name (required)" class="w-full max-w-xs" />
|
||||
<DInput autocomplete="off" @bind-Value="model.Name" Validator="true" placeholder="Name (required)" class="w-full max-w-xs" />
|
||||
<DValidatorHint>Name is required</DValidatorHint>
|
||||
</div>
|
||||
</section>
|
||||
@@ -82,22 +82,22 @@
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
@foreach (var color in Enum.GetValues<DColor>())
|
||||
{
|
||||
<DTextarea @bind-Value="bio" Color="color" placeholder="@color.ToString()" class="min-h-0 h-12" />
|
||||
<DTextarea autocomplete="off" @bind-Value="bio" Color="color" placeholder="@color.ToString()" class="min-h-0 h-12" />
|
||||
}
|
||||
</div>
|
||||
<div class="flex flex-wrap items-start gap-2">
|
||||
<DTextarea @bind-Value="bio" Size="DSize.Xs" placeholder="Xs" class="w-24" />
|
||||
<DTextarea @bind-Value="bio" Size="DSize.Sm" placeholder="Sm" class="w-24" />
|
||||
<DTextarea @bind-Value="bio" placeholder="Md" class="w-24" />
|
||||
<DTextarea @bind-Value="bio" Size="DSize.Lg" placeholder="Lg" class="w-24" />
|
||||
<DTextarea @bind-Value="bio" Size="DSize.Xl" placeholder="Xl" class="w-24" />
|
||||
<DTextarea autocomplete="off" @bind-Value="bio" Size="DSize.Xs" placeholder="Xs" class="w-24" />
|
||||
<DTextarea autocomplete="off" @bind-Value="bio" Size="DSize.Sm" placeholder="Sm" class="w-24" />
|
||||
<DTextarea autocomplete="off" @bind-Value="bio" placeholder="Md" class="w-24" />
|
||||
<DTextarea autocomplete="off" @bind-Value="bio" Size="DSize.Lg" placeholder="Lg" class="w-24" />
|
||||
<DTextarea autocomplete="off" @bind-Value="bio" Size="DSize.Xl" placeholder="Xl" class="w-24" />
|
||||
</div>
|
||||
<div class="flex flex-wrap items-start gap-2">
|
||||
<DTextarea @bind-Value="bio" Ghost="true" placeholder="Ghost" />
|
||||
<DTextarea @bind-Value="bio" disabled placeholder="Disabled" />
|
||||
<DTextarea autocomplete="off" @bind-Value="bio" Ghost="true" placeholder="Ghost" />
|
||||
<DTextarea autocomplete="off" @bind-Value="bio" disabled placeholder="Disabled" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<DTextarea @bind-Value="model.Bio" Immediate="true" Validator="true" placeholder="Bio, 10 to 140 characters" class="w-full" />
|
||||
<DTextarea autocomplete="off" @bind-Value="model.Bio" Immediate="true" Validator="true" placeholder="Bio, 10 to 140 characters" class="w-full" />
|
||||
<DValidatorHint>Between 10 and 140 characters</DValidatorHint>
|
||||
<span class="text-sm opacity-60">@(model.Bio?.Length ?? 0)/140</span>
|
||||
</div>
|
||||
@@ -286,21 +286,21 @@
|
||||
<Legend>Page details</Legend>
|
||||
<ChildContent>
|
||||
<DLabel>Title</DLabel>
|
||||
<DInput @bind-Value="text" placeholder="My awesome page" />
|
||||
<DInput autocomplete="off" @bind-Value="text" placeholder="My awesome page" />
|
||||
<DLabel>Slug</DLabel>
|
||||
<DInput @bind-Value="site" placeholder="my-awesome-page" />
|
||||
<DInput autocomplete="off" @bind-Value="site" placeholder="my-awesome-page" />
|
||||
</ChildContent>
|
||||
<Description>You can edit these later</Description>
|
||||
</DFieldset>
|
||||
<DFieldset disabled>
|
||||
<Legend>Disabled fieldset</Legend>
|
||||
<ChildContent><DInput @bind-Value="text" placeholder="Can't type here" /></ChildContent>
|
||||
<ChildContent><DInput autocomplete="off" @bind-Value="text" placeholder="Can't type here" /></ChildContent>
|
||||
</DFieldset>
|
||||
<DLabel Floating="true" Text="Your email">
|
||||
<DInput @bind-Value="email" placeholder="mail@site.com" />
|
||||
<DInput autocomplete="off" @bind-Value="email" placeholder="mail@site.com" />
|
||||
</DLabel>
|
||||
<DLabel Floating="true" Text="Notes">
|
||||
<DTextarea @bind-Value="bio" placeholder="Notes" />
|
||||
<DTextarea autocomplete="off" @bind-Value="bio" placeholder="Notes" />
|
||||
</DLabel>
|
||||
</section>
|
||||
|
||||
@@ -308,11 +308,11 @@
|
||||
<h3 class="text-lg font-bold">DValidator / DValidatorHint</h3>
|
||||
<p class="text-sm opacity-60">Native rules, checked by the browser once you've edited the field.</p>
|
||||
<div class="flex flex-col">
|
||||
<DInput @bind-Value="email" Type="email" Validator="true" required="true" placeholder="mail@site.com" />
|
||||
<DInput autocomplete="off" @bind-Value="email" Type="email" Validator="true" required="true" placeholder="mail@site.com" />
|
||||
<DValidatorHint>Enter a valid email address</DValidatorHint>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<DInput @bind-Value="username" Validator="true" required="true" pattern="[A-Za-z][A-Za-z0-9\-]*" minlength="3"
|
||||
<DInput autocomplete="off" @bind-Value="username" Validator="true" required="true" pattern="[A-Za-z][A-Za-z0-9\-]*" minlength="3"
|
||||
maxlength="30" placeholder="Username" title="Only letters, numbers or dash" />
|
||||
<DValidatorHint Compact="true">Must be 3 to 30 letters, numbers or dashes (Compact: no space while valid)</DValidatorHint>
|
||||
</div>
|
||||
@@ -375,7 +375,7 @@
|
||||
string framework;
|
||||
DColor? filterColor;
|
||||
string otp;
|
||||
List<string> files = new();
|
||||
List<string> files = [];
|
||||
|
||||
void Submitted(bool valid) => submitResult = valid ? "Valid" : "Invalid: see the messages above";
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
|
||||
record Toast(int Id, DAlertColor Color);
|
||||
|
||||
readonly List<Toast> toasts = new();
|
||||
readonly List<Toast> toasts = [];
|
||||
DToastHorizontal toastHorizontal;
|
||||
DToastVertical toastVertical;
|
||||
int toastCount;
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
<DButton class="join-item">from lg</DButton>
|
||||
</DJoin>
|
||||
<DJoin>
|
||||
<input class="input join-item" placeholder="Email" />
|
||||
<input autocomplete="off" class="input join-item" placeholder="Email" />
|
||||
<select class="select join-item w-28">
|
||||
<option>Filter</option>
|
||||
<option>Sci-fi</option>
|
||||
|
||||
Reference in new issue
Block a user