Skip to content

Commit 7dd199a

Browse files
committed
Add Help & Support pages with changelog and about
- Add Support, Changelog, and About pages - Add GitHubService for fetching releases from GitHub API - Add AppConstants for centralized app URLs and metadata - Implement cache busting with version query params in index.html - Extend Dockerfile.web to replace version placeholder in both files - Update CSP in nginx.conf.template to allow external connections - Add service worker bypass for external domains (GitHub, Google Fonts) - Add UI version display in About page from build metadata - Update sidebar navigation to show Help & Support link
1 parent 1791d65 commit 7dd199a

12 files changed

Lines changed: 862 additions & 9 deletions

File tree

cloud/deploy/Dockerfile.web

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ RUN apk add --no-cache curl
4141
# Copy Blazor WASM published output to nginx html directory
4242
COPY --from=build /app/publish/wwwroot /usr/share/nginx/html
4343

44-
# Replace service worker version placeholder with build timestamp
45-
# This ensures every deploy gets a unique version for cache busting
46-
RUN sed -i "s/__BUILD_VERSION__/$(date -u +%Y%m%d%H%M%S)/" /usr/share/nginx/html/service-worker.js
44+
# Replace version placeholder with build timestamp for cache busting
45+
# This ensures every deploy gets unique URLs for critical assets
46+
RUN BUILD_VERSION=$(date -u +%Y%m%d%H%M%S) && \
47+
sed -i "s/__BUILD_VERSION__/$BUILD_VERSION/g" /usr/share/nginx/html/service-worker.js && \
48+
sed -i "s/__BUILD_VERSION__/$BUILD_VERSION/g" /usr/share/nginx/html/index.html
4749

4850
# Fix permissions for all files (nginx needs read access)
4951
RUN chmod -R 644 /usr/share/nginx/html/* 2>/dev/null || true && \

cloud/deploy/nginx/nginx.conf.template

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ http {
7676
server_tokens off;
7777

7878
map $uri $csp_header {
79-
default "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
79+
default "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net; img-src 'self' data: blob: https:; connect-src 'self' https: wss:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
8080
}
8181

8282
# === HTTPS SERVER (only if SSL enabled) ===
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
namespace LrmCloud.Web;
2+
3+
/// <summary>
4+
/// Application-wide constants including GitHub repository links.
5+
/// </summary>
6+
public static class AppConstants
7+
{
8+
public const string AppName = "LRM Cloud";
9+
public const string AppDescription = "Localization Resource Manager - Cloud Edition";
10+
public const string AppTagline = "Manage your localization files with ease.";
11+
12+
// GitHub repository
13+
public const string GitHubRepo = "nickprotop/LocalizationManager";
14+
public const string GitHubUrl = "https://github.com/nickprotop/LocalizationManager";
15+
public const string GitHubIssuesUrl = "https://github.com/nickprotop/LocalizationManager/issues";
16+
public const string GitHubNewIssueUrl = "https://github.com/nickprotop/LocalizationManager/issues/new?labels=cloud&title=[Cloud]%20";
17+
public const string GitHubDiscussionsUrl = "https://github.com/nickprotop/LocalizationManager/discussions";
18+
public const string GitHubNewDiscussionUrl = "https://github.com/nickprotop/LocalizationManager/discussions/new?category=ideas";
19+
public const string GitHubReleasesUrl = "https://github.com/nickprotop/LocalizationManager/releases";
20+
public const string GitHubReleasesApiUrl = "https://api.github.com/repos/nickprotop/LocalizationManager/releases";
21+
22+
// Documentation
23+
public const string DocsBaseUrl = "https://github.com/nickprotop/LocalizationManager/blob/main/docs";
24+
public const string DocsGettingStartedUrl = "https://github.com/nickprotop/LocalizationManager/blob/main/README.md";
25+
public const string DocsCliUrl = "https://github.com/nickprotop/LocalizationManager/blob/main/docs/COMMANDS.md";
26+
public const string DocsApiUrl = "https://github.com/nickprotop/LocalizationManager/blob/main/docs/API.md";
27+
public const string DocsCloudSyncUrl = "https://github.com/nickprotop/LocalizationManager/blob/main/docs/CLOUD_SYNC.md";
28+
public const string DocsInstallUrl = "https://github.com/nickprotop/LocalizationManager/blob/main/docs/INSTALLATION.md";
29+
public const string DocsTranslationUrl = "https://github.com/nickprotop/LocalizationManager/blob/main/docs/TRANSLATION.md";
30+
31+
// Sponsor
32+
public const string SponsorUrl = "https://www.buymeacoffee.com/nickprotop";
33+
34+
// License
35+
public const string License = "MIT";
36+
public const string LicenseUrl = "https://github.com/nickprotop/LocalizationManager/blob/main/LICENSE";
37+
38+
// Copyright
39+
public const string Copyright = "2025 LRM Cloud";
40+
}

cloud/src/LrmCloud.Web/Layout/MainLayout.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@
9292
</RadzenPanelMenu>
9393
<div style="flex: 1;"></div>
9494
<RadzenPanelMenu Style="margin-bottom: 1rem;">
95-
<RadzenPanelMenuItem Text="Website" Path="/" Icon="home" />
95+
<RadzenPanelMenuItem Text="Help & Support" Path="support" Icon="help_outline" />
9696
</RadzenPanelMenu>
9797
</RadzenSidebar>
9898

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
@page "/about"
2+
@attribute [Authorize]
3+
@inject LrmCloud.Web.Services.GitHubService GitHubService
4+
@inject IJSRuntime JS
5+
6+
<PageTitle>About - @AppConstants.AppName</PageTitle>
7+
8+
<RadzenBreadCrumb>
9+
<RadzenBreadCrumbItem Path="support" Text="Help & Support" />
10+
<RadzenBreadCrumbItem Text="About" />
11+
</RadzenBreadCrumb>
12+
13+
<RadzenStack AlignItems="Radzen.AlignItems.Center" Gap="2rem" Style="padding: 2rem 0;">
14+
<RadzenCard Style="max-width: 600px; width: 100%; text-align: center;">
15+
<RadzenStack AlignItems="Radzen.AlignItems.Center" Gap="1rem">
16+
<RadzenIcon Icon="translate" Style="font-size: 4rem; color: var(--rz-primary);" />
17+
18+
<RadzenText TextStyle="TextStyle.H3">@AppConstants.AppName</RadzenText>
19+
20+
@if (_isLoading)
21+
{
22+
<RadzenSkeleton Style="height: 24px; width: 100px;" />
23+
}
24+
else
25+
{
26+
<RadzenBadge BadgeStyle="BadgeStyle.Info" Text="@($"v{_version}")" />
27+
}
28+
29+
<RadzenText TextStyle="TextStyle.Body1" class="rz-color-secondary" TextAlign="TextAlign.Center">
30+
@AppConstants.AppDescription
31+
</RadzenText>
32+
33+
<RadzenText TextStyle="TextStyle.Body2" class="rz-color-secondary" TextAlign="TextAlign.Center">
34+
@AppConstants.AppTagline
35+
</RadzenText>
36+
</RadzenStack>
37+
</RadzenCard>
38+
39+
<RadzenRow Gap="1rem" JustifyContent="JustifyContent.Center" Style="max-width: 600px; width: 100%;">
40+
<RadzenColumn Size="12" SizeSM="6">
41+
<RadzenCard Style="height: 100%;">
42+
<RadzenStack AlignItems="Radzen.AlignItems.Center" Gap="0.5rem">
43+
<RadzenIcon Icon="code" Style="font-size: 2rem; color: var(--rz-secondary);" />
44+
<RadzenText TextStyle="TextStyle.Subtitle1">Source Code</RadzenText>
45+
<RadzenText TextStyle="TextStyle.Caption" class="rz-color-secondary" TextAlign="TextAlign.Center">
46+
View the source code on GitHub
47+
</RadzenText>
48+
<RadzenButton Variant="Radzen.Variant.Text" Icon="open_in_new" Text="GitHub"
49+
Click="@(() => OpenInNewTab(AppConstants.GitHubUrl))" />
50+
</RadzenStack>
51+
</RadzenCard>
52+
</RadzenColumn>
53+
54+
<RadzenColumn Size="12" SizeSM="6">
55+
<RadzenCard Style="height: 100%;">
56+
<RadzenStack AlignItems="Radzen.AlignItems.Center" Gap="0.5rem">
57+
<RadzenIcon Icon="menu_book" Style="font-size: 2rem; color: var(--rz-info);" />
58+
<RadzenText TextStyle="TextStyle.Subtitle1">Documentation</RadzenText>
59+
<RadzenText TextStyle="TextStyle.Caption" class="rz-color-secondary" TextAlign="TextAlign.Center">
60+
Learn how to use LRM Cloud
61+
</RadzenText>
62+
<RadzenButton Variant="Radzen.Variant.Text" Icon="open_in_new" Text="Docs"
63+
Click="@(() => OpenInNewTab(AppConstants.DocsBaseUrl))" />
64+
</RadzenStack>
65+
</RadzenCard>
66+
</RadzenColumn>
67+
68+
<RadzenColumn Size="12" SizeSM="6">
69+
<RadzenCard Style="height: 100%;">
70+
<RadzenStack AlignItems="Radzen.AlignItems.Center" Gap="0.5rem">
71+
<RadzenIcon Icon="history" Style="font-size: 2rem; color: var(--rz-warning);" />
72+
<RadzenText TextStyle="TextStyle.Subtitle1">Changelog</RadzenText>
73+
<RadzenText TextStyle="TextStyle.Caption" class="rz-color-secondary" TextAlign="TextAlign.Center">
74+
See what's new in this version
75+
</RadzenText>
76+
<RadzenLink Path="changelog" Text="View Changelog" />
77+
</RadzenStack>
78+
</RadzenCard>
79+
</RadzenColumn>
80+
81+
<RadzenColumn Size="12" SizeSM="6">
82+
<RadzenCard Style="height: 100%;">
83+
<RadzenStack AlignItems="Radzen.AlignItems.Center" Gap="0.5rem">
84+
<RadzenIcon Icon="help_outline" Style="font-size: 2rem; color: var(--rz-success);" />
85+
<RadzenText TextStyle="TextStyle.Subtitle1">Support</RadzenText>
86+
<RadzenText TextStyle="TextStyle.Caption" class="rz-color-secondary" TextAlign="TextAlign.Center">
87+
Get help or report issues
88+
</RadzenText>
89+
<RadzenLink Path="support" Text="Get Support" />
90+
</RadzenStack>
91+
</RadzenCard>
92+
</RadzenColumn>
93+
</RadzenRow>
94+
95+
<RadzenCard Style="max-width: 600px; width: 100%;">
96+
<RadzenStack Gap="0.5rem">
97+
<RadzenStack Orientation="Radzen.Orientation.Horizontal" JustifyContent="JustifyContent.SpaceBetween" AlignItems="Radzen.AlignItems.Center">
98+
<RadzenText TextStyle="TextStyle.Body2" class="rz-color-secondary">License</RadzenText>
99+
<RadzenButton Variant="Radzen.Variant.Text" Text="@AppConstants.License" Size="ButtonSize.Small"
100+
Click="@(() => OpenInNewTab(AppConstants.LicenseUrl))" />
101+
</RadzenStack>
102+
<RadzenStack Orientation="Radzen.Orientation.Horizontal" JustifyContent="JustifyContent.SpaceBetween" AlignItems="Radzen.AlignItems.Center">
103+
<RadzenText TextStyle="TextStyle.Body2" class="rz-color-secondary">Version</RadzenText>
104+
<RadzenText TextStyle="TextStyle.Body2">@(_version ?? "...")</RadzenText>
105+
</RadzenStack>
106+
@if (!string.IsNullOrEmpty(_uiVersion))
107+
{
108+
<RadzenStack Orientation="Radzen.Orientation.Horizontal" JustifyContent="JustifyContent.SpaceBetween" AlignItems="Radzen.AlignItems.Center">
109+
<RadzenText TextStyle="TextStyle.Body2" class="rz-color-secondary">UI Build</RadzenText>
110+
<RadzenText TextStyle="TextStyle.Body2">@_uiVersion</RadzenText>
111+
</RadzenStack>
112+
}
113+
<hr style="margin: 0.5rem 0;" />
114+
<RadzenText TextStyle="TextStyle.Caption" class="rz-color-secondary" TextAlign="TextAlign.Center">
115+
@($"\u00a9 {AppConstants.Copyright}. All rights reserved.")
116+
</RadzenText>
117+
</RadzenStack>
118+
</RadzenCard>
119+
120+
<RadzenStack Orientation="Radzen.Orientation.Horizontal" Gap="1rem" JustifyContent="JustifyContent.Center">
121+
<RadzenButton Variant="Radzen.Variant.Outlined" Icon="favorite" Text="Sponsor"
122+
Click="@(() => OpenInNewTab(AppConstants.SponsorUrl))" />
123+
<RadzenButton Variant="Radzen.Variant.Outlined" Icon="star" Text="Star on GitHub"
124+
Click="@(() => OpenInNewTab(AppConstants.GitHubUrl))" />
125+
</RadzenStack>
126+
</RadzenStack>
127+
128+
@code {
129+
private string? _version;
130+
private string? _uiVersion;
131+
private bool _isLoading = true;
132+
133+
protected override async Task OnInitializedAsync()
134+
{
135+
_isLoading = true;
136+
try
137+
{
138+
_version = await GitHubService.GetLatestVersionAsync();
139+
}
140+
catch
141+
{
142+
_version = "unknown";
143+
}
144+
finally
145+
{
146+
_isLoading = false;
147+
}
148+
}
149+
150+
protected override async Task OnAfterRenderAsync(bool firstRender)
151+
{
152+
if (firstRender)
153+
{
154+
try
155+
{
156+
_uiVersion = await JS.InvokeAsync<string?>("lrmVersion.getUiVersion");
157+
StateHasChanged();
158+
}
159+
catch
160+
{
161+
// JS not available
162+
}
163+
}
164+
}
165+
166+
private async Task OpenInNewTab(string url)
167+
{
168+
await JS.InvokeVoidAsync("lrmLinks.openInNewTab", url);
169+
}
170+
}

0 commit comments

Comments
 (0)