|
1 | | -@using LinkDotNet.Blog.Domain |
| 1 | +@using LinkDotNet.Blog.Domain |
2 | 2 | @using LinkDotNet.Blog.Infrastructure |
3 | 3 | @using LinkDotNet.Blog.Infrastructure.Persistence |
4 | 4 | @using LinkDotNet.Blog.Web.Features.Services |
| 5 | +@using LinkDotNet.Blog.Web.Features.ShowBlogPost.Components |
5 | 6 | @using NCronJob |
6 | 7 | @inject IJSRuntime JSRuntime |
7 | 8 | @inject ICacheInvalidator CacheInvalidator |
8 | 9 | @inject IInstantJobRegistry InstantJobRegistry |
9 | 10 | @inject IRepository<ShortCode> ShortCodeRepository |
10 | 11 |
|
11 | 12 | <div class="container"> |
12 | | - <h3 class="fw-bold">@Title</h3> |
13 | | - <EditForm Model="@model" OnValidSubmit="OnValidBlogPostCreatedAsync"> |
14 | | - <DataAnnotationsValidator /> |
15 | | - <div class="form-floating mb-3"> |
16 | | - <input type="text" class="form-control" id="title" placeholder="Title" |
17 | | - @oninput="args => model.Title = args.Value!.ToString()!" value="@model.Title"/> |
18 | | - <label for="title">Title</label> |
19 | | - <ValidationMessage For="() => model.Title"></ValidationMessage> |
20 | | - </div> |
21 | | - <div class="form-floating mb-3"> |
22 | | - <MarkdownTextArea Id="short" Class="form-control" Rows="4" Placeholder="Short Description" |
23 | | - @bind-Value="@model.ShortDescription" |
24 | | - PreviewFunction="ReplaceShortCodes" |
25 | | - ></MarkdownTextArea> |
26 | | - <ValidationMessage For="() => model.ShortDescription"></ValidationMessage> |
27 | | - </div> |
28 | | - <div class="form-floating mb-3 relative"> |
29 | | - <MarkdownTextArea Id="content" Class="form-control" Rows="20" Placeholder="Content" |
30 | | - PreviewFunction="ReplaceShortCodes" |
31 | | - @bind-Value="@model.Content"></MarkdownTextArea> |
32 | | - <ValidationMessage For="() => model.Content"></ValidationMessage> |
33 | | - |
34 | | - <div class="btn-group position-absolute bottom-0 end-0 m-5 extra-buttons"> |
35 | | - <button class="btn btn-primary btn-outlined btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> |
36 | | - More |
37 | | - </button> |
38 | | - <ul class="dropdown-menu"> |
39 | | - @if (shortCodes.Count > 0) |
40 | | - { |
41 | | - <li> |
42 | | - <button type="button" @onclick="OpenShortCodeDialog" class="dropdown-item"> |
43 | | - <span>Get shortcode</span> |
44 | | - </button> |
45 | | - </li> |
46 | | - } |
47 | | - <li><button type="button" class="dropdown-item" @onclick="FeatureDialog.Open">Experimental Features</button></li> |
48 | | - </ul> |
49 | | - </div> |
50 | | - </div> |
51 | | - <div class="form-floating mb-3"> |
52 | | - <InputText type="url" class="form-control" id="preview" placeholder="Preview-Url" @bind-Value="model.PreviewImageUrl"/> |
53 | | - <label for="preview">Preview-Url</label> |
54 | | - <small for="preview" class="form-text text-body-secondary">The primary image which will be used.</small> |
55 | | - <ValidationMessage For="() => model.PreviewImageUrl"></ValidationMessage> |
56 | | - </div> |
57 | | - <div class="form-floating mb-3"> |
58 | | - <InputText type="url" class="form-control" id="fallback-preview" placeholder="Fallback Preview-Url" @bind-Value="model.PreviewImageUrlFallback"/> |
59 | | - <label for="fallback-preview">Fallback Preview-Url</label> |
60 | | - <small for="fallback-preview" class="form-text text-body-secondary">Optional: Used as a fallback if the preview image can't be used by the browser. |
61 | | - <br>For example using a jpg or png as fallback for avif which is not supported in Safari or Edge.</small> |
62 | | - <ValidationMessage For="() => model.PreviewImageUrlFallback"></ValidationMessage> |
63 | | - </div> |
64 | | - <div class="form-floating mb-3"> |
65 | | - <InputDate Type="InputDateType.DateTimeLocal" class="form-control" id="scheduled" |
66 | | - placeholder="Scheduled Publish Date" @bind-Value="model.ScheduledPublishDate" |
67 | | - @bind-Value:after="@(() => model.IsPublished &= !IsScheduled)"/> |
68 | | - <label for="scheduled">Scheduled Publish Date</label> |
69 | | - <small for="scheduled" class="form-text text-body-secondary">If set the blog post will be published at the given date. |
70 | | - A blog post with a schedule date can't be set to published.</small> |
71 | | - <ValidationMessage For="() => model.ScheduledPublishDate"></ValidationMessage> |
72 | | - </div> |
73 | | - <div class="form-check form-switch mb-3"> |
74 | | - <InputCheckbox class="form-check-input" id="published" @bind-Value="model.IsPublished"/> |
75 | | - <label class="form-check-label" for="published">Publish</label><br/> |
76 | | - <small for="published" class="form-text text-body-secondary">If this blog post is only draft or it will be scheduled, uncheck the box.</small> |
77 | | - <ValidationMessage For="() => model.IsPublished"></ValidationMessage> |
78 | | - </div> |
79 | | - <div class="form-floating mb-3"> |
80 | | - <InputText type="text" class="form-control" id="tags" placeholder="Tags" @bind-Value="model.Tags"/> |
81 | | - <label for="tags">Tags (Comma separated)</label> |
82 | | - </div> |
83 | | - @if (BlogPost is not null && !IsScheduled) |
84 | | - { |
85 | | - <div class="form-check form-switch mb-3"> |
86 | | - <InputCheckbox class="form-check-input" id="updatedate" @bind-Value="model.ShouldUpdateDate" /> |
87 | | - <label class="form-check-label" for="updatedate">Update Publish Date</label><br/> |
88 | | - <small for="updatedate" class="form-text text-body-secondary">If set the publish date is set to now, |
89 | | - otherwise its original date.</small> |
90 | | - </div> |
91 | | - } |
92 | | - <div class="mb-3"> |
93 | | - <button class="btn btn-primary position-relative" type="submit" disabled="@(!canSubmit)">Submit</button> |
94 | | - <div class="alert alert-info text-muted form-text mt-3 mb-3"> |
95 | | - The first page of the blog is cached. Therefore, the blog post is not immediately visible. |
96 | | - Head over to <a href="/settings">settings</a> to invalidate the cache or enable the checkmark. |
97 | | - <br> |
98 | | - The option should be enabled if you want to publish the blog post immediately and it should be visible on the first page. |
99 | | - </div> |
100 | | - <div class="form-check form-switch mb-3"> |
101 | | - <InputCheckbox class="form-check-input" id="invalidate-cache" @bind-Value="model.ShouldInvalidateCache"/> |
102 | | - <label class="form-check-label" for="invalidate-cache">Make it visible immediately</label><br/> |
103 | | - </div> |
104 | | - </div> |
105 | | - </EditForm> |
| 13 | + <h3 class="fw-bold">@Title</h3> |
| 14 | + <EditForm Model="@model" OnValidSubmit="OnValidBlogPostCreatedAsync"> |
| 15 | + <DataAnnotationsValidator /> |
| 16 | + <div class="form-floating mb-3"> |
| 17 | + <input type="text" class="form-control" id="title" placeholder="Title" |
| 18 | + @oninput="args => model.Title = args.Value!.ToString()!" value="@model.Title" /> |
| 19 | + <label for="title">Title</label> |
| 20 | + <ValidationMessage For="() => model.Title"></ValidationMessage> |
| 21 | + </div> |
| 22 | + <div class="form-floating mb-3"> |
| 23 | + <MarkdownTextArea Id="short" Class="form-control" Rows="4" Placeholder="Short Description" |
| 24 | + @bind-Value="@model.ShortDescription" |
| 25 | + PreviewFunction="ReplaceShortCodes"></MarkdownTextArea> |
| 26 | + <ValidationMessage For="() => model.ShortDescription"></ValidationMessage> |
| 27 | + </div> |
| 28 | + <div class="form-floating mb-3 relative"> |
| 29 | + <MarkdownTextArea Id="content" Class="form-control" Rows="20" Placeholder="Content" |
| 30 | + PreviewFunction="ReplaceShortCodes" |
| 31 | + @bind-Value="@model.Content"></MarkdownTextArea> |
| 32 | + <ValidationMessage For="() => model.Content"></ValidationMessage> |
| 33 | + |
| 34 | + <div class="btn-group position-absolute bottom-0 end-0 m-5 extra-buttons"> |
| 35 | + <button class="btn btn-primary btn-outlined btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> |
| 36 | + More |
| 37 | + </button> |
| 38 | + <ul class="dropdown-menu"> |
| 39 | + @if (shortCodes.Count > 0) |
| 40 | + { |
| 41 | + <li> |
| 42 | + <button type="button" @onclick="OpenShortCodeDialog" class="dropdown-item"> |
| 43 | + <span>Get shortcode</span> |
| 44 | + </button> |
| 45 | + </li> |
| 46 | + } |
| 47 | + <li><button type="button" class="dropdown-item" @onclick="FeatureDialog.Open">Experimental Features</button></li> |
| 48 | + <li><button id="convert" type="button" class="dropdown-item" @onclick="ConvertContent">@ConvertLabel <i class="lab"></i></button></li> |
| 49 | + </ul> |
| 50 | + </div> |
| 51 | + </div> |
| 52 | + <div class="form-floating mb-3"> |
| 53 | + <InputText type="url" class="form-control" id="preview" placeholder="Preview-Url" @bind-Value="model.PreviewImageUrl" /> |
| 54 | + <label for="preview">Preview-Url</label> |
| 55 | + <small for="preview" class="form-text text-body-secondary">The primary image which will be used.</small> |
| 56 | + <ValidationMessage For="() => model.PreviewImageUrl"></ValidationMessage> |
| 57 | + </div> |
| 58 | + <div class="form-floating mb-3"> |
| 59 | + <InputText type="url" class="form-control" id="fallback-preview" placeholder="Fallback Preview-Url" @bind-Value="model.PreviewImageUrlFallback" /> |
| 60 | + <label for="fallback-preview">Fallback Preview-Url</label> |
| 61 | + <small for="fallback-preview" class="form-text text-body-secondary"> |
| 62 | + Optional: Used as a fallback if the preview image can't be used by the browser. |
| 63 | + <br>For example using a jpg or png as fallback for avif which is not supported in Safari or Edge. |
| 64 | + </small> |
| 65 | + <ValidationMessage For="() => model.PreviewImageUrlFallback"></ValidationMessage> |
| 66 | + </div> |
| 67 | + <div class="form-floating mb-3"> |
| 68 | + <InputDate Type="InputDateType.DateTimeLocal" class="form-control" id="scheduled" |
| 69 | + placeholder="Scheduled Publish Date" @bind-Value="model.ScheduledPublishDate" |
| 70 | + @bind-Value:after="@(() => model.IsPublished &= !IsScheduled)" /> |
| 71 | + <label for="scheduled">Scheduled Publish Date</label> |
| 72 | + <small for="scheduled" class="form-text text-body-secondary"> |
| 73 | + If set the blog post will be published at the given date. |
| 74 | + A blog post with a schedule date can't be set to published. |
| 75 | + </small> |
| 76 | + <ValidationMessage For="() => model.ScheduledPublishDate"></ValidationMessage> |
| 77 | + </div> |
| 78 | + <div class="form-check form-switch mb-3"> |
| 79 | + <InputCheckbox class="form-check-input" id="published" @bind-Value="model.IsPublished" /> |
| 80 | + <label class="form-check-label" for="published">Publish</label><br /> |
| 81 | + <small for="published" class="form-text text-body-secondary">If this blog post is only draft or it will be scheduled, uncheck the box.</small> |
| 82 | + <ValidationMessage For="() => model.IsPublished"></ValidationMessage> |
| 83 | + </div> |
| 84 | + <div class="form-floating mb-3"> |
| 85 | + <InputText type="text" class="form-control" id="tags" placeholder="Tags" @bind-Value="model.Tags" /> |
| 86 | + <label for="tags">Tags (Comma separated)</label> |
| 87 | + </div> |
| 88 | + @if (BlogPost is not null && !IsScheduled) |
| 89 | + { |
| 90 | + <div class="form-check form-switch mb-3"> |
| 91 | + <InputCheckbox class="form-check-input" id="updatedate" @bind-Value="model.ShouldUpdateDate" /> |
| 92 | + <label class="form-check-label" for="updatedate">Update Publish Date</label><br /> |
| 93 | + <small for="updatedate" class="form-text text-body-secondary"> |
| 94 | + If set the publish date is set to now, |
| 95 | + otherwise its original date. |
| 96 | + </small> |
| 97 | + </div> |
| 98 | + } |
| 99 | + <div class="mb-3"> |
| 100 | + <button class="btn btn-primary position-relative" type="submit" disabled="@(!canSubmit)">Submit</button> |
| 101 | + <div class="alert alert-info text-muted form-text mt-3 mb-3"> |
| 102 | + The first page of the blog is cached. Therefore, the blog post is not immediately visible. |
| 103 | + Head over to <a href="/settings">settings</a> to invalidate the cache or enable the checkmark. |
| 104 | + <br> |
| 105 | + The option should be enabled if you want to publish the blog post immediately and it should be visible on the first page. |
| 106 | + </div> |
| 107 | + <div class="form-check form-switch mb-3"> |
| 108 | + <InputCheckbox class="form-check-input" id="invalidate-cache" @bind-Value="model.ShouldInvalidateCache" /> |
| 109 | + <label class="form-check-label" for="invalidate-cache">Make it visible immediately</label><br /> |
| 110 | + </div> |
| 111 | + </div> |
| 112 | + </EditForm> |
106 | 113 | </div> |
107 | 114 |
|
108 | 115 | <FeatureInfoDialog @ref="FeatureDialog"></FeatureInfoDialog> |
|
127 | 134 |
|
128 | 135 | private CreateNewModel model = new(); |
129 | 136 |
|
130 | | - private bool canSubmit = true; |
131 | | - private IPagedList<ShortCode> shortCodes = PagedList<ShortCode>.Empty; |
| 137 | + private string? originalContent = null; |
| 138 | + private bool IsContentConverted => !string.IsNullOrWhiteSpace(originalContent); |
| 139 | + private string ConvertLabel => !IsContentConverted ? "Convert to markdown" : "Restore"; |
132 | 140 |
|
133 | | - private bool IsScheduled => model.ScheduledPublishDate.HasValue; |
| 141 | + private bool canSubmit = true; |
| 142 | + private IPagedList<ShortCode> shortCodes = PagedList<ShortCode>.Empty; |
134 | 143 |
|
135 | | - protected override async Task OnInitializedAsync() |
136 | | - { |
137 | | - shortCodes = await ShortCodeRepository.GetAllAsync(); |
138 | | - } |
| 144 | + private bool IsScheduled => model.ScheduledPublishDate.HasValue; |
| 145 | + |
| 146 | + protected override async Task OnInitializedAsync() |
| 147 | + { |
| 148 | + shortCodes = await ShortCodeRepository.GetAllAsync(); |
| 149 | + } |
139 | 150 |
|
140 | | - protected override void OnParametersSet() |
| 151 | + protected override void OnParametersSet() |
141 | 152 | { |
142 | 153 | if (BlogPost is null) |
143 | 154 | { |
|
149 | 160 |
|
150 | 161 | private async Task OnValidBlogPostCreatedAsync() |
151 | 162 | { |
152 | | - canSubmit = false; |
| 163 | + canSubmit = false; |
153 | 164 | await OnBlogPostCreated.InvokeAsync(model.ToBlogPost()); |
154 | 165 | if (model.ShouldInvalidateCache) |
155 | | - { |
156 | | - CacheInvalidator.Cancel(); |
157 | | - } |
| 166 | + { |
| 167 | + CacheInvalidator.Cancel(); |
| 168 | + } |
158 | 169 |
|
159 | 170 | InstantJobRegistry.RunInstantJob<SimilarBlogPostJob>(parameter: true); |
160 | 171 | ClearModel(); |
161 | | - canSubmit = true; |
| 172 | + canSubmit = true; |
162 | 173 | } |
163 | 174 |
|
164 | 175 | private void ClearModel() |
|
186 | 197 |
|
187 | 198 | private Task<string> ReplaceShortCodes(string markdown) |
188 | 199 | { |
189 | | - foreach (var code in shortCodes) |
190 | | - { |
191 | | - markdown = markdown.Replace($"[[{code.Name}]]", code.MarkdownContent); |
192 | | - } |
| 200 | + foreach (var code in shortCodes) |
| 201 | + { |
| 202 | + markdown = markdown.Replace($"[[{code.Name}]]", code.MarkdownContent); |
| 203 | + } |
193 | 204 |
|
194 | | - return Task.FromResult(MarkdownConverter.ToMarkupString(markdown).Value); |
| 205 | + return Task.FromResult(MarkdownConverter.ToMarkupString(markdown).Value); |
195 | 206 | } |
196 | 207 |
|
197 | 208 | private void OpenShortCodeDialog() |
198 | 209 | { |
199 | | - ShortCodeDialog.Open(); |
200 | | - StateHasChanged(); |
| 210 | + ShortCodeDialog.Open(); |
| 211 | + StateHasChanged(); |
| 212 | + } |
| 213 | + |
| 214 | + /// <summary> |
| 215 | + /// Convert content from HTML to Markdown and viceversa |
| 216 | + /// </summary> |
| 217 | + private void ConvertContent() |
| 218 | + { |
| 219 | + if (IsContentConverted) |
| 220 | + { |
| 221 | + model.Content = originalContent!; |
| 222 | + originalContent = null; |
| 223 | + } |
| 224 | + else |
| 225 | + { |
| 226 | + originalContent = model.Content; |
| 227 | + var converter = new ReverseMarkdown.Converter(); |
| 228 | + model.Content = converter.Convert(model.Content); |
| 229 | + } |
201 | 230 | } |
202 | 231 | } |
0 commit comments