diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bdeb487..9c339db 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -66,5 +66,7 @@ jobs: username: ${{ secrets.SSH_USERNAME }} key: ${{ secrets.SSH_KEY }} script: | + export GOOGLE_ANALYTICS_TAG="${{ secrets.GOOGLE_ANALYTICS_TAG }}" chmod +x blog.stevanfreeborn.com/deploy.ps1 - sudo pwsh ./blog.stevanfreeborn.com/deploy.ps1 ${{ needs.build.outputs.version }} \ No newline at end of file + sudo pwsh ./blog.stevanfreeborn.com/deploy.ps1 ${{ needs.build.outputs.version }} + unset GOOGLE_ANALYTICS_TAG \ No newline at end of file diff --git a/scripts/deploy.ps1 b/scripts/deploy.ps1 index 61d2341..b8b9c8a 100644 --- a/scripts/deploy.ps1 +++ b/scripts/deploy.ps1 @@ -8,7 +8,8 @@ function StartContainer { $containerName = "blog.stevanfreeborn.com.$containerColor" $postDirEnv = "FilePostServiceOptions__PostsDirectory=wwwroot/posts" - $containerId = docker run -d -p 8080 --name $containerName --env $postDirEnv $dockerTag + $googleTagEnv = "GoogleAnalyticsTag=$env:GOOGLE_ANALYTICS_TAG" + $containerId = docker run -d -p 8080 --name $containerName --env $postDirEnv --env $googleTagEnv $dockerTag if ($LASTEXITCODE -ne 0) { diff --git a/src/Blog/Blog.csproj b/src/Blog/Blog.csproj index 3d300cd..765ce12 100644 --- a/src/Blog/Blog.csproj +++ b/src/Blog/Blog.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Blog/Components/App.razor b/src/Blog/Components/App.razor index 9e10592..fcc528d 100644 --- a/src/Blog/Components/App.razor +++ b/src/Blog/Components/App.razor @@ -8,14 +8,17 @@ - + - + + + + diff --git a/src/Blog/Components/Layout/Footer.razor b/src/Blog/Components/Layout/Footer.razor new file mode 100644 index 0000000..87239ba --- /dev/null +++ b/src/Blog/Components/Layout/Footer.razor @@ -0,0 +1,7 @@ +@namespace Blog.Components.Layout + +
+ Code + Terms + Privacy +
\ No newline at end of file diff --git a/src/Blog/Components/Layout/Footer.razor.css b/src/Blog/Components/Layout/Footer.razor.css new file mode 100644 index 0000000..8fdbfc5 --- /dev/null +++ b/src/Blog/Components/Layout/Footer.razor.css @@ -0,0 +1,6 @@ +footer { + display: flex; + justify-content: flex-end; + width: 100%; + gap: 1rem; +} \ No newline at end of file diff --git a/src/Blog/Components/Layout/Header.razor b/src/Blog/Components/Layout/Header.razor index 6d311d8..d7dff6f 100644 --- a/src/Blog/Components/Layout/Header.razor +++ b/src/Blog/Components/Layout/Header.razor @@ -2,9 +2,9 @@
by diff --git a/src/Blog/Components/Layout/MainLayout.razor b/src/Blog/Components/Layout/MainLayout.razor index 99391fc..ac58ee7 100644 --- a/src/Blog/Components/Layout/MainLayout.razor +++ b/src/Blog/Components/Layout/MainLayout.razor @@ -1,11 +1,11 @@ @inherits LayoutComponentBase -@rendermode InteractiveServer
@Body
+
@@ -13,3 +13,10 @@ Reload 🗙
+ +
+
+

Connection lost

+

Attempting to reconnect...

+
+
\ No newline at end of file diff --git a/src/Blog/Components/Layout/MainLayout.razor.css b/src/Blog/Components/Layout/MainLayout.razor.css index 6ddc2c9..ce9a855 100644 --- a/src/Blog/Components/Layout/MainLayout.razor.css +++ b/src/Blog/Components/Layout/MainLayout.razor.css @@ -6,12 +6,14 @@ padding: 1.25rem; width: 100%; gap: 4rem; + flex: 1; } main { display: flex; flex-direction: column; width: 100%; + flex: 1; } #blazor-error-ui { @@ -32,3 +34,11 @@ main { right: 0.75rem; top: 0.5rem; } + +#components-reconnect-modal { + display: none; + position: fixed; + bottom: 0; + right: 0; + padding: 0.5rem; +} \ No newline at end of file diff --git a/src/Blog/Components/Pages/Error.razor b/src/Blog/Components/Pages/Error.razor index c04dc20..f89d2ae 100644 --- a/src/Blog/Components/Pages/Error.razor +++ b/src/Blog/Components/Pages/Error.razor @@ -15,7 +15,8 @@ }
-@code{ +@code +{ [CascadingParameter] private HttpContext? HttpContext { get; set; } diff --git a/src/Blog/Components/Pages/Post.razor b/src/Blog/Components/Pages/Post.razor index d158c90..1628c12 100644 --- a/src/Blog/Components/Pages/Post.razor +++ b/src/Blog/Components/Pages/Post.razor @@ -1,5 +1,4 @@ @page "/{slug:nonfile}" -@implements IAsyncDisposable @inject IPostService PostService @inject NavigationManager NavigationManager @inject IJSRuntime JSRuntime @@ -13,11 +12,12 @@ else { @BlogPost.Title - journal + + -
@@ -31,11 +31,11 @@ else
} -@code { +@code +{ [Parameter] public string Slug { get; set; } = string.Empty; private PostWithContent? BlogPost; - private IJSObjectReference? module; protected override async Task OnInitializedAsync() { @@ -49,32 +49,4 @@ else BlogPost = post; } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - module = await JSRuntime.InvokeAsync("import", "./Components/Pages/Post.razor.js"); - await module.InvokeVoidAsync("addAnchors"); - await module.InvokeVoidAsync("addClipboard"); - } - } - - async ValueTask IAsyncDisposable.DisposeAsync() - { - try - { - if (module is not null) - { - await module.DisposeAsync(); - } - } - catch (Exception ex) when (ex is JSDisconnectedException) - { - } - catch (Exception ex) - { - Logger.LogError(ex, "Error disposing of the module"); - } - } } \ No newline at end of file diff --git a/src/Blog/Components/Pages/Post.razor.js b/src/Blog/Components/Pages/Post.razor.js index 04db9aa..f11ab7a 100644 --- a/src/Blog/Components/Pages/Post.razor.js +++ b/src/Blog/Components/Pages/Post.razor.js @@ -1,9 +1,37 @@ +let initialized = false; + +export function onLoad() { + if (initialized) { + return; + } + + init(); +} + +export function onUpdate() { + if (initialized) { + return; + } + + init(); +} + +export function onDispose() { + initialized = false; +} + +function init(){ + addAnchors(); + addClipboard(); + Prism.highlightAll(); + initialized = true; +} /** * @summary Adds anchors to each heading in the post * so that users can link directly to them. */ -export function addAnchors() { +function addAnchors() { const selectors = [ '.markdown-body h2', '.markdown-body h3', @@ -21,7 +49,7 @@ export function addAnchors() { * @summary Adds a button to each code block that * allows users to copy the code to their clipboard. */ -export function addClipboard() { +function addClipboard() { const codeBlocks = document.querySelectorAll('pre'); codeBlocks.forEach((block) => { diff --git a/src/Blog/Components/Pages/Privacy.razor b/src/Blog/Components/Pages/Privacy.razor new file mode 100644 index 0000000..2eaf4b3 --- /dev/null +++ b/src/Blog/Components/Pages/Privacy.razor @@ -0,0 +1,115 @@ +@page "/privacy" + +
+

Privacy Policy for journal

+ +
+

At journal, accessible from https://blog.stevanfreeborn.com, one of + our main priorities is the privacy of our visitors. This Privacy Policy document contains types of information + that + is collected and recorded by journal and how we use it.

+ +

+ If you have additional questions or require more information about our Privacy Policy, do not hesitate to contact + us. +

+
+ +
+

Log Files

+ +

+ journal follows a standard procedure of using log files. These files log visitors when they visit websites. All + hosting companies do this and a part of hosting services' analytics. The information collected by log files + include + internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, + referring/exit + pages, and possibly the number of clicks. These are not linked to any information that is personally identifiable. + The purpose of the information is for analyzing trends, administering the site, tracking users' movement on the + website, and gathering demographic information. Our Privacy Policy was created with the help of the Privacy Policy Generator. +

+
+ +
+

Cookies and Web Beacons

+ +

+ Like any other website, journal uses "cookies". These cookies are used to store information including visitors' + preferences, and the pages on the website that the visitor accessed or visited. The information is used to + optimize + the users' experience by customizing our web page content based on visitors' browser type and/or other + information. +

+ +

+ For more general information on cookies, please read the "Cookies" article from the Privacy Policy + Generator. +

+
+ +
+

Privacy Policies

+ +

You may consult this list to find the Privacy Policy for each of the advertising partners of journal.

+ +

+ Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web Beacons that are used in + their respective advertisements and links that appear on journal, which are sent directly to users' browser. They + automatically receive your IP address when this occurs. These technologies are used to measure the effectiveness + of + their advertising campaigns and/or to personalize the advertising content that you see on websites that you visit. +

+ +

Note that journal has no access to or control over these cookies that are used by third-party advertisers.

+ +
+ +
+

Third Party Privacy Policies

+ +

+ journal's Privacy Policy does not apply to other advertisers or websites. Thus, we are advising you to consult the + respective Privacy Policies of these third-party ad servers for more detailed information. It may include their + practices and instructions about how to opt-out of certain options. +

+ +

+ You can choose to disable cookies through your individual browser options. To know more detailed information about + cookie management with specific web browsers, it can be found at the browsers' respective websites. What Are + Cookies? +

+
+ +
+

Children's Information

+ +

+ Another part of our priority is adding protection for children while using the internet. We encourage parents and + guardians to observe, participate in, and/or monitor and guide their online activity. +

+ +

+ journal does not knowingly collect any Personal Identifiable Information from children under the age of 13. If you + think that your child provided this kind of information on our website, we strongly encourage you to contact us + immediately and we will do our best efforts to promptly remove such information from our records. +

+
+ +
+

Online Privacy Policy Only

+ +

+ This Privacy Policy applies only to our online activities and is valid for visitors to our website with regards to + the information that they shared and/or collect in journal. This policy is not applicable to any information + collected offline or via channels other than this website. +

+
+ +
+

Consent

+ +

By using our website, you hereby consent to our Privacy Policy and agree to its Terms and Conditions.

+
+
\ No newline at end of file diff --git a/src/Blog/Components/Pages/Privacy.razor.css b/src/Blog/Components/Pages/Privacy.razor.css new file mode 100644 index 0000000..cc6a5d8 --- /dev/null +++ b/src/Blog/Components/Pages/Privacy.razor.css @@ -0,0 +1,16 @@ +.container { + display: flex; + flex-direction: column; + gap: 2rem; + + section { + display: flex; + flex-direction: column; + gap: 1rem; + + ul { + list-style-type: disc; + padding-left: 1rem; + } + } +} \ No newline at end of file diff --git a/src/Blog/Components/Pages/Terms.razor b/src/Blog/Components/Pages/Terms.razor new file mode 100644 index 0000000..42dc04d --- /dev/null +++ b/src/Blog/Components/Pages/Terms.razor @@ -0,0 +1,269 @@ +@page "/terms" + +
+

Terms and Conditions

+ +
+

Welcome to journal!

+ +

These terms and conditions outline the rules and regulations for the use of journal's Website, located at + https://blog.stevanfreeborn.com. +

+ +

+ By accessing this website we assume you accept these terms and conditions. Do not continue to use journal if you + do + not agree to take all of the terms and conditions stated on this page. +

+ +

+ The following terminology applies to these Terms and Conditions, Privacy Statement and Disclaimer Notice and all + Agreements: "Client", "You" and "Your" refers to you, the person log on this website and compliant to the + Company's + terms and conditions. "The Company", "Ourselves", "We", "Our" and "Us", refers to our Company. "Party", "Parties", + or "Us", refers to both the Client and ourselves. All terms refer to the offer, acceptance and consideration of + payment + necessary to undertake the process of our assistance to the Client in the most appropriate manner for the express + purpose of meeting the Client's needs in respect of provision of the Company's stated services, in accordance with + and subject to, prevailing law of Netherlands. Any use of the above terminology or other words in the singular, + plural, + capitalization and/or he/she or they, are taken as interchangeable and therefore as referring to same. Our Terms + and + Conditions were created with the help of the Terms & Conditions Generator. +

+
+ +
+

Cookies

+ +

+ We employ the use of cookies. By accessing journal, you agreed to use cookies in agreement with the journal's + Privacy Policy. +

+ +

+ Most interactive websites use cookies to let us retrieve the user's details for each visit. Cookies are used by + our + website to enable the functionality of certain areas to make it easier for people visiting our website. Some of + our + affiliate/advertising partners may also use cookies. +

+
+ +
+

License

+ +

+ Unless otherwise stated, journal and/or its licensors own the intellectual property rights for all material on + journal. All intellectual property rights are reserved. You may access this from journal for your own personal use + subjected to restrictions set in these terms and conditions. +

+ +

You must not:

+
    +
  • Republish material from journal
  • +
  • Sell, rent or sub-license material from journal
  • +
  • Reproduce, duplicate or copy material from journal
  • +
  • Redistribute content from journal
  • +
+ +

This Agreement shall begin on the date hereof.

+ +

+ Parts of this website offer an opportunity for users to post and exchange opinions and information in certain + areas + of the website. journal does not filter, edit, publish or review Comments prior to their presence on the website. + Comments do not reflect the views and opinions of journal,its agents and/or affiliates. Comments reflect the views + and opinions of the person who post their views and opinions. To the extent permitted by applicable laws, journal + shall + not be liable for the Comments or for any liability, damages or expenses caused and/or suffered as a result of any + use of and/or posting of and/or appearance of the Comments on this website. +

+ +

+ journal reserves the right to monitor all Comments and to remove any Comments which can be considered + inappropriate, offensive or causes breach of these Terms and Conditions. +

+ +

You warrant and represent that:

+ +
    +
  • You are entitled to post the Comments on our website and have all necessary licenses and consents to do so; +
  • +
  • + The Comments do not invade any intellectual property right, including without limitation copyright, patent or + trademark of any third party; +
  • +
  • + The Comments do not contain any defamatory, libelous, offensive, indecent or otherwise unlawful material which + is + an invasion of privacy +
  • +
  • + The Comments will not be used to solicit or promote business or custom or present commercial activities or + unlawful activity. +
  • +
+ +

+ You hereby grant journal a non-exclusive license to use, reproduce, edit and authorize others to use, reproduce + and + edit any of your Comments in any and all forms, formats or media. +

+
+ +
+

Hyperlinking to our Content

+ +

+ The following organizations may link to our Website without prior written approval: +

+ +
    +
  • Government agencies;
  • +
  • Search engines;
  • +
  • News organizations;
  • +
  • + Online directory distributors may link to our Website in the same manner as they hyperlink to the Websites of + other listed businesses; and +
  • +
  • + System wide Accredited Businesses except soliciting non-profit organizations, charity shopping malls, and + charity fundraising groups which may not hyperlink to our Web site. +
  • +
+ +

+ These organizations may link to our home page, to publications or to other Website information so long as the + link: + (a) is not in any way deceptive; (b) does not falsely imply sponsorship, endorsement or approval of the linking + party and its products and/or services; and (c) fits within the context of the linking party's site. +

+ +

We may consider and approve other link requests from the following types of organizations:

+ +
    +
  • commonly-known consumer and/or business information sources;
  • +
  • dot.com community sites;
  • +
  • associations or other groups representing charities;
  • +
  • online directory distributors;
  • +
  • internet portals;
  • +
  • accounting, law and consulting firms; and
  • +
  • educational institutions and trade associations.
  • +
+ +

+ We will approve link requests from these organizations if we decide that: (a) the link would not make us look + unfavorably to ourselves or to our accredited businesses; (b) the organization does not have any negative records + with us; (c) the benefit to us from the visibility of the hyperlink compensates the absence of journal; and (d) + the + link + is in the context of general resource information. +

+ +

+ These organizations may link to our home page so long as the link: (a) is not in any way deceptive; (b) does not + falsely imply sponsorship, endorsement or approval of the linking party and its products or services; and (c) fits + within the context of the linking party's site. +

+ +

+ If you are one of the organizations listed in paragraph 2 above and are interested in linking to our website, you + must inform us by sending an e-mail to journal. Please include your name, your organization name, contact + information as well as the URL of your site, a list of any URLs from which you intend to link to our Website, and + a + list of the + URLs on our site to which you would like to link. Wait 2-3 weeks for a response. +

+ +

Approved organizations may hyperlink to our Website as follows:

+ +
    +
  • By use of our corporate name; or
  • +
  • By use of the uniform resource locator being linked to; or
  • +
  • + By use of any other description of our Website being linked to that makes sense within the context and format of + content on the linking party's site. +
  • +
+ +

No use of journal's logo or other artwork will be allowed for linking absent a trademark license agreement.

+
+ +
+

iFrames

+ +

+ Without prior approval and written permission, you may not create frames around our Webpages that alter in any way + the visual presentation or appearance of our Website. +

+
+ +
+

Content Liability

+ +

+ We shall not be hold responsible for any content that appears on your Website. You agree to protect and defend us + against all claims that is rising on your Website. No link(s) should appear on any Website that may be interpreted + as libelous, obscene or criminal, or which infringes, otherwise violates, or advocates the infringement or other + violation of, any third party rights. +

+
+ +
+

Reservation of Rights

+ +

+ We reserve the right to request that you remove all links or any particular link to our Website. You approve to + immediately remove all links to our Website upon request. We also reserve the right to amen these terms and + conditions and it's linking policy at any time. By continuously linking to our Website, you agree to be bound to + and + follow + these linking terms and conditions. +

+
+ +
+

Removal of links from our website

+ +

+ If you find any link on our Website that is offensive for any reason, you are free to contact and inform us any + moment. We will consider requests to remove links but we are not obligated to or so or to respond to you directly. +

+ +

+ We do not ensure that the information on this website is correct, we do not warrant its completeness or accuracy; + nor do we promise to ensure that the website remains available or that the material on the website is kept up to + date. +

+
+ +
+

Disclaimer

+ +

+ To the maximum extent permitted by applicable law, we exclude all representations, warranties and conditions + relating to our website and the use of this website. Nothing in this disclaimer will: +

+ +
    +
  • limit or exclude our or your liability for death or personal injury;
  • +
  • limit or exclude our or your liability for fraud or fraudulent misrepresentation;
  • +
  • limit any of our or your liabilities in any way that is not permitted under applicable law; or
  • +
  • exclude any of our or your liabilities that may not be excluded under applicable law.
  • +
+ +

+ The limitations and prohibitions of liability set in this Section and elsewhere in this disclaimer: (a) are + subject + to the preceding paragraph; and (b) govern all liabilities arising under the disclaimer, including liabilities + arising in contract, in tort and for breach of statutory duty. +

+ +

+ As long as the website and the information and services on the website are provided free of charge, we will not be + liable for any loss or damage of any nature. +

+
+
\ No newline at end of file diff --git a/src/Blog/Components/Pages/Terms.razor.css b/src/Blog/Components/Pages/Terms.razor.css new file mode 100644 index 0000000..cc6a5d8 --- /dev/null +++ b/src/Blog/Components/Pages/Terms.razor.css @@ -0,0 +1,16 @@ +.container { + display: flex; + flex-direction: column; + gap: 2rem; + + section { + display: flex; + flex-direction: column; + gap: 1rem; + + ul { + list-style-type: disc; + padding-left: 1rem; + } + } +} \ No newline at end of file diff --git a/src/Blog/Components/Posts/Feed.razor b/src/Blog/Components/Posts/Feed.razor index ae9ed17..d00bb47 100644 --- a/src/Blog/Components/Posts/Feed.razor +++ b/src/Blog/Components/Posts/Feed.razor @@ -18,19 +18,22 @@
@foreach (var post in Posts) { - - }
} -@code { +@code +{ private List? Posts; + + private string GetLink(Post post) => $"/{post.Slug}"; protected override async Task OnInitializedAsync() { diff --git a/src/Blog/Components/Posts/Feed.razor.css b/src/Blog/Components/Posts/Feed.razor.css index f3beb49..cea38a5 100644 --- a/src/Blog/Components/Posts/Feed.razor.css +++ b/src/Blog/Components/Posts/Feed.razor.css @@ -4,43 +4,45 @@ width: 100%; gap: 2rem; - & a { - padding: 1rem; - border-radius: 1rem; - background-color: var(--color-background-soft); - transition-property: transform; - transition-duration: 100ms; - transition-timing-function: ease-in-out - } - - & a:active { - transform: scale(0.95) - } - & article { display: flex; flex-direction: column; - gap: 0.25rem; - & h3 { - background: linear-gradient(to right top, #db3e3e, #df3741, #e32e44, #e72347, #eb124b); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - font-size: 2rem; - font-weight: 900; - } - - & .date { - font-size: 0.85rem; + & a { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 1rem; + border-radius: 1rem; + background-color: var(--color-background-soft); + transition-property: transform; + transition-duration: 100ms; + transition-timing-function: ease-in-out; + + & h3 { + background: linear-gradient(to right top, #db3e3e, #df3741, #e32e44, #e72347, #eb124b); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + font-size: 2rem; + font-weight: 900; + } + + & .date { + font-size: 0.85rem; + } + + & h3, + & .date, + & .lead { + text-wrap: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } } - & h3, - & .date, - & .lead { - text-wrap: nowrap; - overflow: hidden; - text-overflow: ellipsis; + & a:active { + transform: scale(0.95) } } } \ No newline at end of file diff --git a/src/Blog/Components/Utils/PageScript.razor b/src/Blog/Components/Utils/PageScript.razor new file mode 100644 index 0000000..06c80fc --- /dev/null +++ b/src/Blog/Components/Utils/PageScript.razor @@ -0,0 +1,19 @@ +@namespace Blog.Components.Utils + +@* + custom web-component defined in: + Blog.lib.module.js + + Manages loading colocated scripts + for pages and invoking expected + lifecycle methods to support + enhanced page navigation. +*@ + + +@code +{ + [Parameter] + [EditorRequired] + public string Src { get; set; } = default!; +} \ No newline at end of file diff --git a/src/Blog/Components/_Imports.razor b/src/Blog/Components/_Imports.razor index cc46e1b..32f5171 100644 --- a/src/Blog/Components/_Imports.razor +++ b/src/Blog/Components/_Imports.razor @@ -8,3 +8,6 @@ @using Microsoft.JSInterop @using Blog @using Blog.Components +@using Blog.Components.Utils +@using Blazor.Analytics +@using Blazor.Analytics.Components \ No newline at end of file diff --git a/src/Blog/Program.cs b/src/Blog/Program.cs index 05f5b87..65bb5ad 100644 --- a/src/Blog/Program.cs +++ b/src/Blog/Program.cs @@ -1,12 +1,15 @@ +using Blazor.Analytics; + var builder = WebApplication.CreateBuilder(args); builder.Services.ConfigureOptions(); builder.Services.AddSingleton(); builder.Services.AddScoped(); -builder.Services - .AddRazorComponents() - .AddInteractiveServerComponents(); +builder.Services.AddRazorComponents(); + +var googleTag = builder.Configuration["GoogleAnalyticsTag"]; +builder.Services.AddGoogleAnalytics(googleTag); var app = builder.Build(); @@ -88,10 +91,7 @@ app .WithDisplayName("RSS Feed") .WithDescription("RSS feed for the blog"); -app - .MapRazorComponents() - .AddInteractiveServerRenderMode(); - +app.MapRazorComponents(); app.Run(); diff --git a/src/Blog/appsettings.example.json b/src/Blog/appsettings.example.json index 33ba1f0..a731b84 100644 --- a/src/Blog/appsettings.example.json +++ b/src/Blog/appsettings.example.json @@ -10,5 +10,6 @@ "FilePostServiceOptions": { "PostsDirectory": "wwwroot/posts" }, - "DetailedErrors": "true" + "DetailedErrors": "true", + "GoogleAnalyticsTag": "" } \ No newline at end of file diff --git a/src/Blog/wwwroot/Blog.lib.module.js b/src/Blog/wwwroot/Blog.lib.module.js new file mode 100644 index 0000000..7415504 --- /dev/null +++ b/src/Blog/wwwroot/Blog.lib.module.js @@ -0,0 +1,95 @@ +const pageScriptInfoBySrc = new Map(); + +function registerPageScriptElement(src) { + if (!src) { + throw new Error('Must provide a non-empty value for the "src" attribute.'); + } + + let pageScriptInfo = pageScriptInfoBySrc.get(src); + + if (pageScriptInfo) { + pageScriptInfo.referenceCount++; + return; + } + + pageScriptInfo = { referenceCount: 1, module: null }; + pageScriptInfoBySrc.set(src, pageScriptInfo); + initializePageScriptModule(src, pageScriptInfo); +} + +function unregisterPageScriptElement(src) { + if (!src) { + return; + } + + const pageScriptInfo = pageScriptInfoBySrc.get(src); + + if (!pageScriptInfo) { + return; + } + + pageScriptInfo.referenceCount--; +} + +async function initializePageScriptModule(src, pageScriptInfo) { + // If the path is relative, normalize it by by making it an absolute URL + // with document's the base HREF. + if (src.startsWith("./")) { + src = new URL(src.substr(2), document.baseURI).toString(); + } + + const module = await import(src); + + if (pageScriptInfo.referenceCount <= 0) { + // All page-script elements with the same 'src' were + // unregistered while we were loading the module. + return; + } + + pageScriptInfo.module = module; + module.onLoad?.(); + module.onUpdate?.(); +} + +function onEnhancedLoad() { + // Start by invoking 'onDispose' on any modules that are no longer referenced. + for (const [src, { module, referenceCount }] of pageScriptInfoBySrc) { + if (referenceCount <= 0) { + module?.onDispose?.(); + pageScriptInfoBySrc.delete(src); + } + } + + // Then invoke 'onUpdate' on the remaining modules. + for (const { module } of pageScriptInfoBySrc.values()) { + module?.onUpdate?.(); + } +} + +export function afterWebStarted(blazor) { + customElements.define( + "page-script", + class extends HTMLElement { + static observedAttributes = ["src"]; + + // We use attributeChangedCallback instead of connectedCallback + // because a page-script element might get reused between enhanced + // navigations. + attributeChangedCallback(name, oldValue, newValue) { + if (name !== "src") { + return; + } + + this.src = newValue; + unregisterPageScriptElement(oldValue); + registerPageScriptElement(newValue); + } + + disconnectedCallback() { + unregisterPageScriptElement(this.src); + } + } + ); + + blazor.addEventListener("enhancedload", onEnhancedLoad); +} diff --git a/test/Blog.Tests/Unit/Footer.spec.razor b/test/Blog.Tests/Unit/Footer.spec.razor new file mode 100644 index 0000000..fd6a6fa --- /dev/null +++ b/test/Blog.Tests/Unit/Footer.spec.razor @@ -0,0 +1,23 @@ +@inherits Bunit.TestContext + +@code { + [Test] + public void Footer_WhenRendered_ItShouldRenderFooterWithNavLinks() + { + var ctx = new TestContext(); + + var cut = ctx.Render(@