Skip to content

Commit ef6a6ae

Browse files
committed
Merge branch 'main' into on-the-fly
2 parents a3e062d + 53c2872 commit ef6a6ae

47 files changed

Lines changed: 377 additions & 207 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-publish.yml

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ name: Build and Publish
55
# 2. android - Builds the Android app and uploads it to Google Play (if requested)
66
# 3. docker - Builds the Docker image and pushes it to GitHub Container Registry
77
# 4. ios - Builds the iOS app and uploads it to App Store (if requested)
8-
# 5. github-release - Tags the commit and creates a Github release with the apps' binaries (if requested)
8+
# 5. tag - Tags the built commit with the version string
9+
# 6. github-release - Creates a Github release with the apps' binaries (if requested)
910

1011
on:
1112
workflow_dispatch:
@@ -52,9 +53,21 @@ jobs:
5253
PATCH: ${{ github.run_number }}
5354
run: |
5455
version_string=$MAJOR.$MINOR.$(($PATCH % 1000))
55-
echo "version_code=$(($MAJOR * 100000 + $MINOR * 1000 + $PATCH % 1000))" >> $GITHUB_OUTPUT
56+
version_code=$(($MAJOR * 100000 + $MINOR * 1000 + $PATCH % 1000))
57+
echo "version_code=$version_code" >> $GITHUB_OUTPUT
5658
echo "version_string=$version_string" >> $GITHUB_OUTPUT
57-
echo "Version: $version_string" >> $GITHUB_STEP_SUMMARY
59+
commit_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$GITHUB_SHA"
60+
{
61+
echo "### Build and Publish $version_string"
62+
echo ""
63+
echo "| | |"
64+
echo "| --- | --- |"
65+
echo "| Version | \`$version_string\` |"
66+
echo "| Version code | \`$version_code\` |"
67+
echo "| Commit | [\`$(git rev-parse --short HEAD)\`]($commit_url) - $(git log -1 --pretty=%s) |"
68+
echo "| Ref | \`$GITHUB_REF_NAME\` |"
69+
echo "| Docker image | \`ghcr.io/israelhikingmap/website-mapeak:$version_string\` |"
70+
} >> $GITHUB_STEP_SUMMARY
5871
- name: Get all milestones
5972
id: get_milestones
6073
uses: octokit/request-action@v3.0.0
@@ -233,17 +246,32 @@ jobs:
233246
env:
234247
APPSTORE_CONNECT_API_KEY: ${{ secrets.APPSTORE_CONNECT_API_KEY }}
235248

236-
github-release:
237-
if: ${{ github.event.inputs.production == 'true' }}
249+
tag:
250+
if: ${{ github.ref == 'refs/heads/main' }}
238251
runs-on: ubuntu-latest
239252
needs: [version, ios, android]
253+
permissions:
254+
contents: write
240255
steps:
241256
- name: Checkout code
242257
uses: actions/checkout@v7.0.1
243258
- name: Tag commit and push
259+
env:
260+
VERSION: ${{ needs.version.outputs.version_string }}
244261
run: |
245-
git tag "v${{ needs.version.outputs.version_string }}"
246-
git push origin "v${{ needs.version.outputs.version_string }}"
262+
git tag "v$VERSION"
263+
git push origin "v$VERSION"
264+
tag_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/tree/v$VERSION"
265+
commit_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$GITHUB_SHA"
266+
echo "Tagged [\`v$VERSION\`]($tag_url) at [\`$(git rev-parse --short HEAD)\`]($commit_url)" >> $GITHUB_STEP_SUMMARY
267+
268+
github-release:
269+
if: ${{ github.event.inputs.production == 'true' && github.ref == 'refs/heads/main' }}
270+
runs-on: ubuntu-latest
271+
needs: [version, ios, android, tag]
272+
steps:
273+
- name: Checkout code
274+
uses: actions/checkout@v7.0.1
247275
- name: Download artifacts
248276
uses: actions/download-artifact@v8
249277
with:

IsraelHiking.Web/Program.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Net.Http;
55
using System.Reflection;
66
using System.Text.Json.Serialization;
7+
using System.Text.RegularExpressions;
78
using System.Threading.Tasks;
89
using IsraelHiking.API;
910
using IsraelHiking.API.Services;
@@ -56,7 +57,8 @@ void SetupApplication(WebApplication app)
5657
ContentTypeProvider = new FileExtensionContentTypeProvider
5758
{
5859
Mappings = { { ".pbf", "application/x-protobuf" } } // for the fonts files
59-
}
60+
},
61+
OnPrepareResponse = SetStaticFileCacheHeaders
6062
});
6163
app.MapOpenApi();
6264
app.MapScalarApiReference("/openapi", options => options.AddPreferredSecuritySchemes("Bearer"));
@@ -143,6 +145,26 @@ void SetupServices(IServiceCollection services, bool isDevelopment)
143145
});
144146
}
145147

148+
/// <summary>
149+
/// Files built by the angular CLI carry a content hash in their name, so they can be cached forever.
150+
/// Everything else keeps its name across deployments and is only cached for a day.
151+
/// HTML is never cached since it points at the hashed file names.
152+
/// </summary>
153+
void SetStaticFileCacheHeaders(StaticFileResponseContext context)
154+
{
155+
var path = context.Context.Request.Path.Value ?? string.Empty;
156+
if (path.EndsWith(".html", StringComparison.OrdinalIgnoreCase))
157+
{
158+
context.Context.Response.Headers.CacheControl = "no-cache";
159+
return;
160+
}
161+
var isHashed = path.StartsWith("/media/", StringComparison.OrdinalIgnoreCase) ||
162+
Regex.IsMatch(path, @"-[A-Z0-9]{8}\.(js|mjs|css)$", RegexOptions.IgnoreCase);
163+
context.Context.Response.Headers.CacheControl = isHashed
164+
? "public, max-age=31536000, immutable"
165+
: "public, max-age=86400";
166+
}
167+
146168
void InitializeServices(IServiceProvider serviceProvider)
147169
{
148170
var logger = serviceProvider.GetRequiredService<ILogger>();

IsraelHiking.Web/package-lock.json

Lines changed: 0 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

IsraelHiking.Web/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
"dexie": "^4.4.4",
5656
"fflate": "^0.8.3",
5757
"file-saver-es": "^2.0.5",
58-
"font-awesome": "^4.7.0",
5958
"geojson-path-finder": "^2.1.0",
6059
"immer": "^11.1.15",
6160
"intl": "^1.2.5",
@@ -146,4 +145,4 @@
146145
},
147146
"type": "module",
148147
"version": "9.21.0"
149-
}
148+
}

IsraelHiking.Web/src/application/app.routes.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import { Route } from "@angular/router";
1+
import { inject } from "@angular/core";
2+
import { ResolveFn, Route } from "@angular/router";
23
import { environment } from "../environments/environment";
4+
import { MapService } from "./services/map.service";
5+
6+
/**
7+
* Maplibre, its workers and the protocols used by the map styles are only loaded for routes that
8+
* actually show a map, so that content-only screens (landing, faq, etc.) do not download them.
9+
* The router waits for this resolver before activating the component, so maplibre is always ready
10+
* before a style starts loading.
11+
*/
12+
const initializeMapResolver: ResolveFn<void> = () => inject(MapService).initialize();
313

414
export const routes: Route[] = [
515
{ path: "", redirectTo: environment.isCapacitor ? "/map" : "/about", pathMatch: "full", title: "Mapeak" },
@@ -21,12 +31,14 @@ export const routes: Route[] = [
2131
{
2232
path: "offline-management",
2333
loadComponent: () => import("./components/screens/offline-management.component").then(m => m.OfflineManagementComponent),
24-
title: "Mapeak - Offline Management"
34+
title: "Mapeak - Offline Management",
35+
resolve: { map: initializeMapResolver }
2536
},
2637
{
2738
path: "public-routes",
2839
loadComponent: () => import("./components/screens/public-routes.component").then(m => m.PublicRoutesComponent),
29-
title: "Mapeak - Public Routes"
40+
title: "Mapeak - Public Routes",
41+
resolve: { map: initializeMapResolver }
3042
},
3143
{
3244
path: "privacy-policy",
@@ -36,16 +48,19 @@ export const routes: Route[] = [
3648
{
3749
path: "shares",
3850
loadComponent: () => import("./components/screens/shares.component").then(m => m.SharesComponent),
39-
title: "Mapeak - Cloud Saves"
51+
title: "Mapeak - Cloud Saves",
52+
resolve: { map: initializeMapResolver }
4053
},
4154
{
4255
path: "traces",
4356
loadComponent: () => import("./components/screens/traces.component").then(m => m.TracesComponent),
44-
title: "Mapeak - Traces"
57+
title: "Mapeak - Traces",
58+
resolve: { map: initializeMapResolver }
4559
},
4660
{
4761
path: "**",
4862
loadComponent: () => import("./components/map/main-map.component").then(m => m.MainMapComponent),
49-
title: "Mapeak"
63+
title: "Mapeak",
64+
resolve: { map: initializeMapResolver }
5065
}
5166
];

IsraelHiking.Web/src/application/components/main-menu.component.html

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@
1010
<span class="mx-1">{{resources.findRoutes}}</span>
1111
</button>
1212
}
13-
@if (!isIFrame() && !isLoggedIn()) {
13+
@if (isUserKnown() && !isIFrame() && !isLoggedIn()) {
1414
<button mat-raised-button (click)="login()">{{resources.signIn}}</button>
1515
}
1616
<button mat-button [matMenuTriggerFor]="appMenu" analyticsOn="click" analyticsCategory="Menu"
1717
analyticsLabel="Toggle main menu" class="!h-[56px] text-white!">
1818
<span class="flex flex-row justify-center items-center">
19-
@if (isLoggedIn()) {
19+
@if (!isUserKnown()) {
20+
<!-- Holds the avatar's place until we know who the user is, so the toolbar does not jump -->
21+
<span class="max-h-[48px] w-[48px] me-4"></span>
22+
} @else if (isLoggedIn()) {
2023
@if (userInfo()?.imageUrl) {
21-
<img [src]="userInfo()?.imageUrl" alt="" class="max-h-[48px] me-4 rounded-full" />
24+
<img [src]="userInfo()?.imageUrl" alt="" class="max-h-[48px] w-[48px] me-4 rounded-full" />
2225
} @else {
2326
<i class="fa icon-user-circle fa-lg me-4"></i>
2427
}

IsraelHiking.Web/src/application/components/main-menu.component.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, inject, computed } from "@angular/core";
1+
import { Component, inject, computed, signal, afterNextRender } from "@angular/core";
22
import { RouterLink, RouterLinkActive } from "@angular/router";
33
import { MatButton } from "@angular/material/button";
44
import { MatMenuTrigger, MatMenu, MatMenuItem } from "@angular/material/menu";
@@ -53,7 +53,17 @@ export class MainMenuComponent {
5353

5454
public readonly isLoggedIn = computed(() => this.userInfo() != null);
5555

56+
/**
57+
* Whether the signed in user is known yet. The content routes are prerendered at build time,
58+
* where there is never a user, so the prerendered html must not claim the visitor is signed out -
59+
* otherwise a signed in visitor stares at a "sign in" button until the persisted state is read
60+
* out of indexeddb. This stays false through hydration so the client's first render still matches
61+
* the prerendered markup, and flips right after it.
62+
*/
63+
public readonly isUserKnown = signal(false);
64+
5665
constructor() {
66+
afterNextRender(() => this.isUserKnown.set(true));
5767
if (this.runningContextService.isCapacitor) {
5868
App.getInfo().then((info) => {
5969
this.loggingService.info(`App version: ${info.version}`);

IsraelHiking.Web/src/application/components/public-routes-filter.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
<div class="flex flex-row justify-center">
9797
<button mat-button class="mx-2 rounded-full! bg-gray-200! p-2 text-black!" (click)="clearUserFilter()">
9898
{{filterUserName()}}
99-
<i class="fa fa-times mx-2"></i>
99+
<i class="fa icon-close mx-2"></i>
100100
</button>
101101
</div>
102102
}

IsraelHiking.Web/src/application/components/screens/landing.component.html

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1-
<header class="relative h-screen min-h-screen w-full bg-cover bg-center bg-no-repeat"
2-
style="background-image: url('content/hero-mountain.jpg');">
1+
<header class="relative h-screen min-h-screen w-full overflow-hidden">
2+
<picture>
3+
<source type="image/avif" sizes="100vw"
4+
srcset="content/hero-mountain-640.avif 640w, content/hero-mountain-960.avif 960w, content/hero-mountain-1280.avif 1280w, content/hero-mountain-1920.avif 1920w">
5+
<source type="image/webp" sizes="100vw"
6+
srcset="content/hero-mountain-640.webp 640w, content/hero-mountain-960.webp 960w, content/hero-mountain-1280.webp 1280w, content/hero-mountain-1920.webp 1920w">
7+
<!-- An img rather than a css background so the preload scanner finds it while parsing the
8+
html, and can pick a size that fits the screen instead of always sending the full one. -->
9+
<img src="content/hero-mountain-1280.jpg" alt="" width="1920" height="1080" fetchpriority="high"
10+
class="absolute inset-0 h-full w-full object-cover object-center">
11+
</picture>
312
<div class="absolute inset-0 bg-black/10"></div>
413
<div class="relative z-10 flex h-full flex-col items-center justify-center px-4 text-center text-white">
514
<h1 class="mb-6 text-5xl font-extrabold leading-tight tracking-tight md:text-7xl">
@@ -14,18 +23,18 @@ <h1 class="mb-6 text-5xl font-extrabold leading-tight tracking-tight md:text-7xl
1423
<div class="flex flex-col gap-4 sm:flex-row">
1524
<a [href]="androidAppUrl" analyticsLabel="Download from google play store"
1625
class="group flex items-center gap-3 rounded-lg transition-all hover:scale-105">
17-
<img src="content/google-play-badge.png" alt="Google Play" width="200">
26+
<img src="content/google-play-badge.png" alt="Google Play" width="200" height="59">
1827
</a>
1928

2029
<a [href]="iosAppUrl" analyticsLabel="Download from app store"
2130
class="group flex items-center gap-3 rounded-lg transition-all hover:scale-105">
22-
<img src="content/app-store-badge.png" alt="App Store" width="200">
31+
<img src="content/app-store-badge.png" alt="App Store" width="200" height="59">
2332
</a>
2433
</div>
2534
}
2635
</div>
2736
<div class="absolute bottom-16 left-1/2 -translate-x-1/2 animate-bounce text-gray-100">
28-
<i class="fa fa-chevron-down fa-lg"></i>
37+
<i class="fa icon-chevron-down fa-lg"></i>
2938
</div>
3039
</header>
3140

@@ -149,7 +158,7 @@ <h3 class="text-xl font-bold text-gray-900 uppercase tracking-wide">Mapeak Pro</
149158
</li>
150159
<li class="flex items-center gap-3 font-semibold text-gray-900">
151160
<span
152-
class="fa fa-battery-full border-2 border-solid border-gray-200 p-1 rounded-full aspect-square flex! items-center"></span>
161+
class="fa icon-battery border-2 border-solid border-gray-200 p-1 rounded-full aspect-square flex! items-center"></span>
153162
<span>Save Battery on Long Trails</span>
154163
</li>
155164
</ul>

IsraelHiking.Web/src/application/reducers/configuration.reducer.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,51 +7,51 @@ import type { ConfigurationState, Language, BatteryOptimizationType, Theme } fro
77

88

99
export class SetLanguageAction {
10-
public static readonly type = this.prototype.constructor.name;
10+
public static readonly type = "[Configuration] SetLanguageAction";
1111
constructor(public readonly language: Language) { }
1212
}
1313

1414
export class SetBatteryOptimizationTypeAction {
15-
public static readonly type = this.prototype.constructor.name;
15+
public static readonly type = "[Configuration] SetBatteryOptimizationTypeAction";
1616
constructor(public readonly batteryOptimizationType: BatteryOptimizationType) { }
1717
}
1818

1919
export class ToggleAutomaticRecordingUploadAction {
20-
public static readonly type = this.prototype.constructor.name;
20+
public static readonly type = "[Configuration] ToggleAutomaticRecordingUploadAction";
2121
}
2222

2323
export class ToggleGotLostWarningsAction {
24-
public static readonly type = this.prototype.constructor.name;
24+
public static readonly type = "[Configuration] ToggleGotLostWarningsAction";
2525
}
2626

2727
export class ToggleIsShowSlopeAction {
28-
public static readonly type = this.prototype.constructor.name;
28+
public static readonly type = "[Configuration] ToggleIsShowSlopeAction";
2929
}
3030

3131
export class ToggleIsShowKmMarkersAction {
32-
public static readonly type = this.prototype.constructor.name;
32+
public static readonly type = "[Configuration] ToggleIsShowKmMarkersAction";
3333
}
3434

3535
export class StopShowingBatteryConfirmationAction {
36-
public static readonly type = this.prototype.constructor.name;
36+
public static readonly type = "[Configuration] StopShowingBatteryConfirmationAction";
3737
}
3838

3939
export class StopShowingIntroAction {
40-
public static readonly type = this.prototype.constructor.name;
40+
public static readonly type = "[Configuration] StopShowingIntroAction";
4141
}
4242

4343
export class SetUnitsAction {
44-
public static readonly type = this.prototype.constructor.name;
44+
public static readonly type = "[Configuration] SetUnitsAction";
4545
constructor(public readonly units: "metric" | "imperial") { }
4646
}
4747

4848
export class SetDateFormatAction {
49-
public static readonly type = this.prototype.constructor.name;
49+
public static readonly type = "[Configuration] SetDateFormatAction";
5050
constructor(public readonly dateFormat: string) { }
5151
}
5252

5353
export class SetThemeAction {
54-
public static readonly type = this.prototype.constructor.name;
54+
public static readonly type = "[Configuration] SetThemeAction";
5555
constructor(public readonly theme: Theme) { }
5656
}
5757

0 commit comments

Comments
 (0)