From eca91affe359f0e869a52bc6aa3492a800a33e67 Mon Sep 17 00:00:00 2001 From: hokiepokedad2 <38219945+hokiepokedad2@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:35:00 -0400 Subject: [PATCH] fix(where): Places matches the site, and places can be made in place MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Style The page used a mat-list; every comparable page in the app uses a card grid with an accent bar, skeleton loaders and a dashed empty state. It now matches Geofences, which is the closest sibling. The pin appears as the first card rather than a banner above the list, because it IS a place — the unnamed one every alert falls back to — and a separate treatment said otherwise. Its accent and icon are muted, since it is the fallback and not a choice. Dead end The scope picker's empty state read "Add one from the location menu", naming a menu removed when Places became a page. Wrong in all eleven locales. Rather than just correct the words, the select now ends with Add a place: it opens the map picker, asks for a name, saves and selects it. Creating a place is only ever wanted at that exact moment, and sending someone to another screen lost the alarm they were mid-edit on — thirteen steps across two contexts, down to eight in one. Also added a check that reads the keys templates ask for and asserts each exists in en.json, then that every locale carries them. Parity against English cannot catch a key English is missing too, which is how NAV.PLACES went absent from all eleven. It reports two pre-existing gaps, COMMON.ERROR and COMMON.SAVED, left alone here. --- .../app/modules/places/places.component.html | 92 ++++++--- .../app/modules/places/places.component.scss | 177 +++++++++++++++--- .../app/modules/places/places.component.ts | 8 +- .../where-sheet/where-sheet.component.html | 6 +- .../where-sheet/where-sheet.component.ts | Bin 5144 -> 7397 bytes .../ClientApp/src/assets/i18n/da.json | 4 +- .../ClientApp/src/assets/i18n/de.json | 4 +- .../ClientApp/src/assets/i18n/en.json | 4 +- .../ClientApp/src/assets/i18n/es.json | 4 +- .../ClientApp/src/assets/i18n/fr.json | 4 +- .../ClientApp/src/assets/i18n/it.json | 4 +- .../ClientApp/src/assets/i18n/nl.json | 4 +- .../ClientApp/src/assets/i18n/pl.json | 4 +- .../ClientApp/src/assets/i18n/pt-BR.json | 4 +- .../ClientApp/src/assets/i18n/pt.json | 4 +- .../ClientApp/src/assets/i18n/sv.json | 4 +- CHANGELOG.md | 2 + 17 files changed, 262 insertions(+), 67 deletions(-) diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.html index 64fa6319..681a40df 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.html @@ -3,47 +3,83 @@

{{ 'WHERE.PLACES_TITLE' | translate }}

{{ 'WHERE.PLACES_PAGE_DESC' | translate }}

- +
+ +
@if (loading()) { -
-} @else { -
- @if (places.pin(); as pin) { -
- my_location -
- {{ 'WHERE.PIN_TITLE' | translate }} - {{ pin.latitude | number: '1.4-4' }}, {{ pin.longitude | number: '1.4-4' }} +
+ @for (i of skeletons; track i) { +
+
+
+
+
+
+
-

{{ 'WHERE.PIN_NOTE' | translate }}

} +
+} @else { +
+ +
+
+
+
+ my_location +
+ {{ 'WHERE.PIN_TITLE' | translate }} + @if (places.pin(); as pin) { + {{ pin.latitude | number: '1.4-4' }}, {{ pin.longitude | number: '1.4-4' }} + } @else { + {{ 'WHERE.PIN_UNSET' | translate }} + } +
+
+

{{ 'WHERE.PIN_NOTE' | translate }}

+
+
- @if (places.named().length === 0) { -

{{ 'WHERE.PLACES_EMPTY' | translate }}

- } @else { - - @for (place of places.named(); track place.label) { - - place - {{ place.label }} - {{ place.latitude | number: '1.4-4' }}, {{ place.longitude | number: '1.4-4' }} + @for (place of places.named(); track place.label) { +
+
+
+
+ place +
+ {{ place.label }} + {{ place.latitude | number: '1.4-4' }}, {{ place.longitude | number: '1.4-4' }} +
+
+ +
+
}
+ + @if (places.named().length === 0) { +
+ add_location_alt +

{{ 'WHERE.PLACES_EMPTY_TITLE' | translate }}

+

{{ 'WHERE.PLACES_EMPTY' | translate }}

+ +
+ } } diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.scss b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.scss index a07dfe39..f8283c0c 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.scss +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.scss @@ -1,43 +1,172 @@ -.places { - display: block; - min-width: min(26rem, 80vw); +.place-grid { + display: grid; + gap: 16px; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + padding: 0 24px; } -.places-loading { +.place-card { + background: var(--card-bg, #fff); + border: 1px solid var(--card-border, rgb(0 0 0 / 12%)); + border-radius: 12px; + box-shadow: 0 1px 3px rgb(0 0 0 / 6%); + overflow: hidden; + position: relative; + transition: + transform 0.2s, + box-shadow 0.2s; + + &:hover { + box-shadow: 0 4px 12px rgb(0 0 0 / 10%); + transform: translateY(-2px); + } +} + +.place-card-accent { + background: #e91e63; + height: 4px; +} + +// The pin is the fallback rather than a choice, so it reads a shade quieter than the places +// the user deliberately made. +.place-card-pin .place-card-accent { + background: var(--text-secondary, rgb(0 0 0 / 38%)); +} + +.place-card-pin .place-card-icon { + color: var(--text-secondary, rgb(0 0 0 / 54%)); +} + +.place-card-body { + padding: 16px 16px 12px; +} + +.place-card-header { + align-items: flex-start; display: flex; - justify-content: center; - padding: 2rem 0; + gap: 10px; } -.places-pin { - align-items: center; +.place-card-icon { + color: #e91e63; + flex-shrink: 0; + font-size: 22px; + height: 22px; + margin-top: 1px; + width: 22px; +} + +.place-card-info { display: flex; - gap: 0.75rem; - padding: 0.5rem 0; + flex: 1; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.place-card-name { + font-size: 15px; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.place-card-coords { + color: var(--text-secondary, rgb(0 0 0 / 54%)); + font-size: 12px; + font-variant-numeric: tabular-nums; +} + +.place-card-note { + color: var(--text-secondary, rgb(0 0 0 / 54%)); + font-size: 12px; + margin: 10px 0 0; } -.places-pin-icon { - opacity: 0.7; +.place-card-footer { + border-top: 1px solid var(--divider, rgb(0 0 0 / 6%)); + display: flex; + justify-content: flex-end; + margin-top: 10px; + padding-top: 4px; } -.places-pin-text { +.place-empty-state { + align-items: center; + background: var(--card-bg, rgb(0 0 0 / 1%)); + border: 2px dashed var(--card-border, rgb(0 0 0 / 12%)); + border-radius: 12px; display: flex; flex-direction: column; + margin: 16px 24px 0; + padding: 32px 24px; + text-align: center; +} + +.place-empty-icon { + color: var(--text-secondary, rgb(0 0 0 / 38%)); + font-size: 40px; + height: 40px; + margin-bottom: 8px; + width: 40px; +} + +.place-empty-title { + font-size: 15px; + font-weight: 500; + margin: 0 0 4px; +} + +.place-empty-subtitle { + color: var(--text-secondary, rgb(0 0 0 / 54%)); + font-size: 13px; + margin: 0 0 16px; + max-width: 40ch; +} + +.skeleton-place-card { + animation: pulse 1.5s ease-in-out infinite; + background: var(--card-bg, #fff); + border: 1px solid var(--card-border, rgb(0 0 0 / 12%)); + border-radius: 12px; + border-top: 4px solid var(--skeleton-bg, rgb(0 0 0 / 8%)); + padding: 14px 16px; +} + +.skeleton-place-header { + align-items: center; + display: flex; + gap: 10px; } -.places-pin-detail, -.places-pin-note, -.places-empty { - color: var(--mat-sys-on-surface-variant, rgb(0 0 0 / 60%)); - font-size: 0.8rem; +.skeleton-place-icon { + background: var(--skeleton-bg, rgb(0 0 0 / 8%)); + border-radius: 50%; + height: 22px; + width: 22px; } -.places-pin-note { - border-bottom: 1px solid var(--mat-sys-outline-variant, rgb(0 0 0 / 12%)); - margin: 0 0 0.5rem; - padding-bottom: 0.75rem; +.skeleton-place-lines { + display: flex; + flex: 1; + flex-direction: column; + gap: 8px; } -.places-empty { - margin: 1rem 0; +.skeleton-line { + background: var(--skeleton-bg, rgb(0 0 0 / 8%)); + border-radius: 6px; + height: 12px; +} + +@media (max-width: 599px) { + .place-grid { + grid-template-columns: 1fr; + padding: 0 16px; + } + + .place-empty-state { + margin: 16px; + } } diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.ts index a2f74706..9991c8aa 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.ts +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/modules/places/places.component.ts @@ -4,9 +4,8 @@ import { ChangeDetectionStrategy, Component, OnInit, inject, signal } from '@ang import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; -import { MatListModule } from '@angular/material/list'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { Location, SavedPlace } from '../../core/models'; @@ -28,7 +27,7 @@ import { LocationDialogComponent } from '../../shared/components/location-dialog */ @Component({ changeDetection: ChangeDetectionStrategy.OnPush, - imports: [DecimalPipe, MatButtonModule, MatIconModule, MatListModule, MatProgressSpinnerModule, TranslatePipe], + imports: [DecimalPipe, MatButtonModule, MatIconModule, MatTooltipModule, TranslatePipe], selector: 'app-places', standalone: true, styleUrl: './places.component.scss', @@ -39,8 +38,11 @@ export class PlacesComponent implements OnInit { private readonly snackBar = inject(MatSnackBar); private readonly translate = inject(TranslateService); readonly busy = signal(false); + readonly loading = signal(true); readonly places = inject(PlacesService); + /** Placeholder count while loading: enough to fill a row without implying how many you have. */ + readonly skeletons = [0, 1, 2]; addPlace(): void { const picker = this.dialog.open(LocationDialogComponent, { diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/where-sheet/where-sheet.component.html b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/where-sheet/where-sheet.component.html index f4b89fcd..b247cc0d 100644 --- a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/where-sheet/where-sheet.component.html +++ b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/where-sheet/where-sheet.component.html @@ -19,11 +19,15 @@

{{ 'WHERE.SHEET_TITLE' | translate }}

{{ 'WHERE.PLACE_LABEL' | translate }} - + {{ 'WHERE.MY_PIN' | translate }} @for (place of places.named(); track place.label) { {{ place.label }} } + + add_location_alt + {{ 'WHERE.ADD_PLACE' | translate }} + diff --git a/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/where-sheet/where-sheet.component.ts b/Applications/Pgan.PoracleWebNet.App/ClientApp/src/app/shared/components/where-sheet/where-sheet.component.ts index 48feb64d5273c1675921c805e02bfc9460025b5f..4e9541406d2653c59ca416e2dcacdff925be0447 100644 GIT binary patch delta 2202 zcmb7FO>ZMb5G6qz;s|yZ?PX6}8zejq-f@Hw(k2Qha!f#*j}^Ay7^JV|$za!Zx%CEEkwSS1K@lge?}wz#vOlgj=BG zh?2PvhN&JJ(3nu_5Sj`JqUaUAz%|HRBu@c{s9?l|K?-Di8q=rGc*P@`)k=nRASi)3l|9c7}R0tSzTSsUU?QA#bCsWc; zjg+2hjHRvV;PCML$?-w6MZ8pBB8U$i!0Pnpjr(hp-zr~Ex|_|(_nVKWe{FubHu?P3 z7t_jX->v7w6P#rzv`g*oK*pm>i?DCYBgQOYjwcOKUlODOnf;Nia)OKMgc^<xjib;El&}f_7+2g+qXCZ*GY?SVkjnv~_07%)q>iL)F|}{N65HY{u8po! z-ymWl!X&X>Oppx;q2+|slmgSXaU-T(ZwSAj7G_0?8H{N7d=rr+K^T5nQ?nc_o|9jrM0=V~TD-oBR?NFA})?@8iy z<^?6c@4W5!E$uU&%QILd*qz(KD?`LuF_(KQ9YM93pPihXordj$Pg>Plec?Kd zk!C&W*G*SH{jl|MD?B|oY5Ac)KWVkw2S+PmXicP<`G8{*6E6$#!RtFGO`j6UdX~sZ zQiqmO>Ufy4EXZurvM{kST`CbsRb>0O65UEVw}mbv1fXS*+ig&-E+QW<9!Ryx{~@9C zQRjFmPZ{lcQRmmG+QA}KcUhtlz9_cUHRAQ8l-e!%bxoUbNvkuk14vZS=#_KmMkjo; zFf2`_|5lDEj3dvOgO*v_Jy3S@&?aTP*EhgM%uX=vCHEN6tDvQOT2gjN z``bTuK(7wkSB##V0b!#6*5Y@UjSV22U;{U*a~Q{dNdYZek9pptak+Tuzv-{9@2~v> DKX<