Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,83 @@
<h1>{{ 'WHERE.PLACES_TITLE' | translate }}</h1>
<p class="page-description">{{ 'WHERE.PLACES_PAGE_DESC' | translate }}</p>
</div>
<button mat-flat-button [disabled]="busy() || loading()" (click)="addPlace()">
<mat-icon>add</mat-icon>
{{ 'WHERE.ADD_PLACE' | translate }}
</button>
<div class="page-header-actions">
<button mat-fab extended color="primary" [disabled]="busy() || loading()" (click)="addPlace()">
<mat-icon>add_location_alt</mat-icon>
{{ 'WHERE.ADD_PLACE' | translate }}
</button>
</div>
</div>

@if (loading()) {
<div class="loading-container"><mat-spinner diameter="48" /></div>
} @else {
<div class="places">
@if (places.pin(); as pin) {
<div class="places-pin">
<mat-icon class="places-pin-icon">my_location</mat-icon>
<div class="places-pin-text">
<span class="places-pin-title">{{ 'WHERE.PIN_TITLE' | translate }}</span>
<span class="places-pin-detail">{{ pin.latitude | number: '1.4-4' }}, {{ pin.longitude | number: '1.4-4' }}</span>
<div class="place-grid">
@for (i of skeletons; track i) {
<div class="skeleton-place-card">
<div class="skeleton-place-header">
<div class="skeleton-place-icon"></div>
<div class="skeleton-place-lines">
<div class="skeleton-line" style="width: 55%"></div>
<div class="skeleton-line" style="width: 75%"></div>
</div>
</div>
</div>
<p class="places-pin-note">{{ 'WHERE.PIN_NOTE' | translate }}</p>
}
</div>
} @else {
<div class="place-grid">
<!-- The pin is a place too, just the unnamed one every alert falls back to. Showing it in the
same grid says that, where a separate banner would imply it is a different kind of thing. -->
<div class="place-card place-card-pin">
<div class="place-card-accent"></div>
<div class="place-card-body">
<div class="place-card-header">
<mat-icon class="place-card-icon">my_location</mat-icon>
<div class="place-card-info">
<span class="place-card-name">{{ 'WHERE.PIN_TITLE' | translate }}</span>
@if (places.pin(); as pin) {
<span class="place-card-coords">{{ pin.latitude | number: '1.4-4' }}, {{ pin.longitude | number: '1.4-4' }}</span>
} @else {
<span class="place-card-coords">{{ 'WHERE.PIN_UNSET' | translate }}</span>
}
</div>
</div>
<p class="place-card-note">{{ 'WHERE.PIN_NOTE' | translate }}</p>
</div>
</div>

@if (places.named().length === 0) {
<p class="places-empty">{{ 'WHERE.PLACES_EMPTY' | translate }}</p>
} @else {
<mat-list>
@for (place of places.named(); track place.label) {
<mat-list-item>
<mat-icon matListItemIcon>place</mat-icon>
<span matListItemTitle>{{ place.label }}</span>
<span matListItemLine>{{ place.latitude | number: '1.4-4' }}, {{ place.longitude | number: '1.4-4' }}</span>
@for (place of places.named(); track place.label) {
<div class="place-card">
<div class="place-card-accent"></div>
<div class="place-card-body">
<div class="place-card-header">
<mat-icon class="place-card-icon">place</mat-icon>
<div class="place-card-info">
<span class="place-card-name">{{ place.label }}</span>
<span class="place-card-coords">{{ place.latitude | number: '1.4-4' }}, {{ place.longitude | number: '1.4-4' }}</span>
</div>
</div>
<div class="place-card-footer">
<button
mat-icon-button
matListItemMeta
[disabled]="busy()"
[attr.aria-label]="'WHERE.PLACE_DELETE_TITLE' | translate"
[matTooltip]="'WHERE.PLACE_DELETE_TITLE' | translate"
(click)="confirmRemove(place)">
<mat-icon>delete</mat-icon>
</button>
</mat-list-item>
}
</mat-list>
</div>
</div>
</div>
}
</div>

@if (places.named().length === 0) {
<div class="place-empty-state">
<mat-icon class="place-empty-icon">add_location_alt</mat-icon>
<p class="place-empty-title">{{ 'WHERE.PLACES_EMPTY_TITLE' | translate }}</p>
<p class="place-empty-subtitle">{{ 'WHERE.PLACES_EMPTY' | translate }}</p>
<button mat-stroked-button color="primary" [disabled]="busy()" (click)="addPlace()">
<mat-icon>add</mat-icon> {{ 'WHERE.ADD_PLACE' | translate }}
</button>
</div>
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
Expand All @@ -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, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ <h2 mat-dialog-title>{{ 'WHERE.SHEET_TITLE' | translate }}</h2>
<div class="where-option-body">
<mat-form-field appearance="outline">
<mat-label>{{ 'WHERE.PLACE_LABEL' | translate }}</mat-label>
<mat-select [ngModel]="placeLabel()" (ngModelChange)="placeLabel.set($event)">
<mat-select [ngModel]="placeLabel()" (ngModelChange)="onTargetChange($event)">
<mat-option value="">{{ 'WHERE.MY_PIN' | translate }}</mat-option>
@for (place of places.named(); track place.label) {
<mat-option [value]="place.label">{{ place.label }}</mat-option>
}
<mat-option [value]="ADD_PLACE">
<mat-icon class="where-add-icon">add_location_alt</mat-icon>
{{ 'WHERE.ADD_PLACE' | translate }}
</mat-option>
</mat-select>
</mat-form-field>

Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,8 @@
"LAST_PAGE": "Sidste side"
},
"WHERE": {
"PLACES_EMPTY_TITLE": "Ingen steder endnu",
"PIN_UNSET": "Ikke angivet",
"PLACES_PAGE_DESC": "Navngivne punkter, dine beskeder kan rettes mod i stedet for din position.",
"ADD_PLACE": "Tilføj et sted",
"AREAS_LABEL": "Områder",
Expand All @@ -1734,7 +1736,7 @@
"NAME_PLACE_TITLE": "Navngiv stedet",
"NEAR_PIN": "Inden for {{distance}} km fra min position",
"NEAR_PLACE": "Inden for {{distance}} km fra {{place}}",
"NO_PLACES": "Du har ikke gemt nogen steder endnu. Tilføj et fra positionsmenuen.",
"NO_PLACES": "Ingen steder endnu. Tilføj et nedenfor for at rette beskeden et andet sted hen end din position.",
"ONLY_IN": "Kun i {{areas}}",
"OPTION_AREAS": "Kun i bestemte områder",
"OPTION_NEAR": "Nær et punkt",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,8 @@
"LAST_PAGE": "Letzte Seite"
},
"WHERE": {
"PLACES_EMPTY_TITLE": "Noch keine Orte",
"PIN_UNSET": "Nicht gesetzt",
"PLACES_PAGE_DESC": "Benannte Punkte, auf die deine Meldungen statt auf deinen Standort ausgerichtet werden können.",
"ADD_PLACE": "Ort hinzufügen",
"AREAS_LABEL": "Gebiete",
Expand All @@ -1734,7 +1736,7 @@
"NAME_PLACE_TITLE": "Ort benennen",
"NEAR_PIN": "Im Umkreis von {{distance}} km um meinen Standort",
"NEAR_PLACE": "Im Umkreis von {{distance}} km um {{place}}",
"NO_PLACES": "Du hast noch keine Orte gespeichert. Füge einen im Standortmenü hinzu.",
"NO_PLACES": "Noch keine Orte. Füge unten einen hinzu, um diese Meldung woandershin als an deinen Standort zu richten.",
"ONLY_IN": "Nur in {{areas}}",
"OPTION_AREAS": "Nur in bestimmten Gebieten",
"OPTION_NEAR": "In der Nähe eines Punktes",
Expand Down
Loading
Loading