Skip to content

UI ux fixes - #20

Merged
wojciech-dron merged 8 commits into
masterfrom
ui-ux-fixes
Mar 11, 2026
Merged

wojciech-dron merged 8 commits into
masterfrom
ui-ux-fixes

Conversation

@wojciech-dron

Copy link
Copy Markdown
Owner

No description provided.

@github-actions

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 Security concerns

Sensitive information exposure:
The newly added JobScraper.Tests/Features/Cv/PdfGeneration/CV_content.md contains realistic-looking personal data (name, phone number, email, LinkedIn/GitHub URLs). If this is not clearly synthetic/test-only content, it could be treated as PII in the repository; consider sanitizing or adding an explicit note that the data is fictitious.

⚡ Recommended focus areas for review

Binding/UX

Multiple inputs add a Step attribute; please verify it matches the bound property types. In particular, Step="0.1" on integer-backed InputNumber fields can cause parsing/validation issues, and Step on InputText is not meaningful and may produce unexpected markup or warnings.

        <InputNumber @bind-Value="Config.TextBlocksAlignedWithImage" class="form-control"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Height">Height</span>
        <InputNumber @bind-Value="Config.ImageHeight" class="form-control"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted"
              title="Space From Text - minimal size of the gap between text and image">Space From Text</span>
        <InputNumber @bind-Value="Config.ImageMinSpaceFromText" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Border Radius">Border Radius</span>
        <InputNumber @bind-Value="Config.ImageBorderRadius" class="form-control"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Border Thick">Border Thick</span>
        <InputNumber @bind-Value="Config.ImageBorderThickness" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Border Color">Border Color</span>
        <input type="color" class="form-control form-control-color p-0" @bind="Config.ImageBorderColor"/>
    </div>
</div>

<div class="col-12"><h6 class="mb-0 mt-2 fw-bold text-muted small uppercase">Font Sizes & Headers</h6></div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Default Size">Default Size</span>
        <InputNumber @bind-Value="Config.DefaultFontSize" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="H1">H1</span>
        <InputNumber @bind-Value="Config.H1FontSize" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="H1 Color">H1 Color</span>
        <input type="color" class="form-control form-control-color p-0" @bind="Config.H1FontColor" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="H2">H2</span>
        <InputNumber @bind-Value="Config.H2FontSize" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="H3">H3</span>
        <InputNumber @bind-Value="Config.H3FontSize" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="H4">H4</span>
        <InputNumber @bind-Value="Config.H4FontSize" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted"
              title="Default Header Size">Default Header Size</span>
        <InputNumber @bind-Value="Config.HeadingFontSizeDefault" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted"
              title="Default Header Color">Default Header Color</span>
        <input type="color" class="form-control form-control-color p-0" @bind="Config.HeadingFontColorDefault"/>
    </div>
</div>

<div class="col-12"><h6 class="mb-0 mt-2 fw-bold text-muted small uppercase">Header Underlines</h6></div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted"
              title="Max Level - maximum level of header that will have included underline">Max Level</span>
        <InputNumber @bind-Value="Config.HeaderUnderlineMaxLevel" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Pad Top">Pad Top</span>
        <InputNumber @bind-Value="Config.HeaderUnderlinePaddingTop" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Color">Color</span>
        <input type="color" class="form-control form-control-color p-0" @bind="Config.HeaderUnderlineColor"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Thick">Thick</span>
        <InputNumber @bind-Value="Config.HeaderUnderlineThickness" class="form-control" Step="0.1"/>
    </div>
</div>

<div class="col-12"><h6 class="mb-0 mt-2 fw-bold text-muted small uppercase">Disclaimer</h6></div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Size">Size</span>
        <InputNumber @bind-Value="Config.DisclaimerFontSize" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Color">Color</span>
        <input type="color" class="form-control form-control-color p-0" @bind="Config.DisclaimerColor"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Footer Pad Bottom">Footer Pad Bottom</span>
        <InputNumber @bind-Value="Config.FooterPaddingBottom" class="form-control"/>
    </div>
</div>

<div class="col-12"><h6 class="mb-0 mt-2 fw-bold text-muted small uppercase">Spaces</h6></div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Header Pad Top">Header Pad Top</span>
        <InputNumber @bind-Value="Config.HeaderPaddingTop" class="form-control"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Line Height">Line Height</span>
        <InputNumber @bind-Value="Config.LineHeight" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Heading Pad Top">Heading Pad Top</span>
        <InputNumber @bind-Value="Config.HeadingPaddingTop" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted"
              title="Heading Pad Bottom">Heading Pad Bottom</span>
        <InputNumber @bind-Value="Config.HeadingPaddingBottom" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted"
              title="Paragraph Pad Bottom">Paragraph Pad Bottom</span>
        <InputNumber @bind-Value="Config.ParagraphPaddingBottom" class="form-control" Step="0.1"/>
    </div>
</div>

<div class="col-12"><h6 class="mb-0 mt-2 fw-bold text-muted small uppercase">Lists</h6></div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Pad Left">Pad Left</span>
        <InputNumber @bind-Value="Config.ListPaddingLeft" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Bullet Text">Bullet Text</span>
        <InputText @bind-Value="Config.ListBulletText" class="form-control" Step="0.1"/>
    </div>
</div>
<div class="flex-col">
    <div class="input-group input-group-sm">
        <span class="input-group-text px-1 py-0 small text-muted" title="Bullet Width">Bullet Width</span>
        <InputNumber @bind-Value="Config.ListBulletWidth" class="form-control" Step="0.1"/>
    </div>
</div>
Possible Issue

LinkInline rendering now creates a hyperlink with link.Url ?? "". If Url is null/empty, this can create an invalid/undesired hyperlink target. Consider guarding and falling back to plain text when the URL is missing, and verify the LineBreakInline { IsHard: false } pattern matches the Markdig version used (to avoid missing soft line breaks).

case LineBreakInline { IsHard: false }:
    text.Span(" ");
    break;
case LineBreakInline:
    break;
case HtmlInline html:
    text.Span(html.Tag);
    break;
case LinkInline link:
    text.Hyperlink(GetInlineText(link), link.Url ?? "")
        .FontColor(layoutConfig.UrlColor)
        .Underline();
    break;
Edge Case

The in-memory SQLite detection builds a SqliteConnectionStringBuilder even when the underlying connection is not SQLite (cast can be null). This can mark non-SQLite connections as “in-memory” due to default/empty values and change behavior compared to the previous check; validate the intended behavior and consider explicitly handling the non-SQLite/null case.

public static async Task PrepareDbAsync(this IServiceProvider services)
{
    using var scope = services.CreateScope();
    var dbContext = scope.ServiceProvider.GetRequiredService<JobsDbContext>();

    var sqliteConn = dbContext.Database.GetDbConnection() as SqliteConnection;
    var csb = new SqliteConnectionStringBuilder(sqliteConn?.ConnectionString);
    var isInMemory = csb.Mode == SqliteOpenMode.Memory || csb.DataSource is "" or ":memory:";
    if (!isInMemory)
        EnsureDbDirectoryExists(dbContext);

@github-actions

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Guard SQLite-only connection parsing

Avoid constructing SqliteConnectionStringBuilder with a null connection string,
which will throw when the underlying connection is not SQLite. Only build the
SQLite-specific logic when the connection is actually a SqliteConnection, and skip
directory creation for non-SQLite providers.

JobScraper.Web/Modules/Persistence/Setup.cs [53-57]

-var sqliteConn = dbContext.Database.GetDbConnection() as SqliteConnection;
-var csb = new SqliteConnectionStringBuilder(sqliteConn?.ConnectionString);
-var isInMemory = csb.Mode == SqliteOpenMode.Memory || csb.DataSource is "" or ":memory:";
-if (!isInMemory)
-    EnsureDbDirectoryExists(dbContext);
+var dbConnection = dbContext.Database.GetDbConnection();
 
+if (dbConnection is SqliteConnection sqliteConn)
+{
+    var csb = new SqliteConnectionStringBuilder(sqliteConn.ConnectionString);
+    var isInMemory = csb.Mode == SqliteOpenMode.Memory || csb.DataSource is "" or ":memory:";
+    if (!isInMemory)
+        EnsureDbDirectoryExists(dbContext);
+}
+
Suggestion importance[1-10]: 8

__

Why: As written, new SqliteConnectionStringBuilder(sqliteConn?.ConnectionString) can throw if GetDbConnection() is not a SqliteConnection (or if the connection string is null), so guarding on dbConnection is SqliteConnection prevents a runtime failure. Skipping EnsureDbDirectoryExists(dbContext) for non-SQLite providers is consistent with this being SQLite-specific setup logic.

Medium
Add fallback for JS import

This path change can break the page at runtime if the JS module file wasn’t renamed
to match the new convention. Add a fallback import path so the feature keeps working
across environments/build artifacts.

JobScraper.Web/Features/Cv/PrepareCvWithAiPage.razor.cs [68-69]

-module = await js.InvokeAsync<IJSObjectReference>("import",
-    $"./Features/Cv/{nameof(PrepareCvWithAiPage)}.razor.js");
+try
+{
+    module = await js.InvokeAsync<IJSObjectReference>(
+        "import",
+        $"./Features/Cv/{nameof(PrepareCvWithAiPage)}.razor.js");
+}
+catch
+{
+    module = await js.InvokeAsync<IJSObjectReference>(
+        "import",
+        "./Features/Cv/PrepareCvWithAi.razor.js");
+}
Suggestion importance[1-10]: 4

__

Why: A fallback import can reduce breakage if the JS module path differs between builds, but the blanket catch may hide real module-loading errors and make failures harder to diagnose. This is a defensive compatibility tweak rather than a clear bug fix based on the diff alone.

Low
Security
Validate URLs before linking

Creating hyperlinks with an empty/invalid link.Url can throw or generate broken
PDFs, and it can also embed unsafe URI schemes. Validate the URL and fall back to
rendering plain text when it’s missing/invalid or uses a disallowed scheme.

JobScraper.Web/Features/Cv/PdfGeneration/GenerateCvPdfFromMarkdown.cs [244-248]

 case LinkInline link:
-    text.Hyperlink(GetInlineText(link), link.Url ?? "")
-        .FontColor(layoutConfig.UrlColor)
-        .Underline();
+    var label = GetInlineText(link);
+
+    if (Uri.TryCreate(link.Url, UriKind.Absolute, out var uri) &&
+        (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeMailto))
+    {
+        text.Hyperlink(label, uri.ToString())
+            .FontColor(layoutConfig.UrlColor)
+            .Underline();
+    }
+    else
+    {
+        text.Span(label);
+    }
+
     break;
Suggestion importance[1-10]: 7

__

Why: Validating link.Url before calling text.Hyperlink(...) improves robustness (avoids empty/invalid URLs) and reduces risk of embedding undesirable schemes like javascript: in generated PDFs. The proposed fallback to rendering a plain Span is a reasonable safe behavior when the URL is missing or invalid.

Medium

@wojciech-dron
wojciech-dron merged commit 4a7c973 into master Mar 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant