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
4 changes: 1 addition & 3 deletions apps/angular/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@
"outputPath": "dist/weather-app-angular",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": [
"zone.js"
],
"polyfills": [],
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
Expand Down
22,612 changes: 9,345 additions & 13,267 deletions apps/angular/package-lock.json

Large diffs are not rendered by default.

27 changes: 13 additions & 14 deletions apps/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,23 @@
"test:ui": "playwright test --ui"
},
"dependencies": {
"@angular/animations": "^17.0.0",
"@angular/common": "^17.0.0",
"@angular/compiler": "^17.0.0",
"@angular/core": "^17.0.0",
"@angular/forms": "^17.0.0",
"@angular/platform-browser": "^17.0.0",
"@angular/platform-browser-dynamic": "^17.0.0",
"@angular/router": "^17.0.0",
"@angular/animations": "^21.2.11",
"@angular/common": "^21.2.11",
"@angular/compiler": "^21.2.11",
"@angular/core": "^21.2.11",
"@angular/forms": "^21.2.11",
"@angular/platform-browser": "^21.2.11",
"@angular/platform-browser-dynamic": "^21.2.11",
"@angular/router": "^21.2.11",
Comment on lines +13 to +20
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.0"
"tslib": "^2.8.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.0.0",
"@angular/cli": "^17.0.0",
"@angular/compiler-cli": "^17.0.0",
"@angular-devkit/build-angular": "^21.2.9",
"@angular/cli": "^21.2.9",
Comment on lines +25 to +26
"@angular/compiler-cli": "^21.2.11",
"@playwright/test": "^1.40.0",
"typescript": "~5.2.0"
"typescript": "~5.9.3"
},
"keywords": [
"weather",
Expand Down
47 changes: 13 additions & 34 deletions apps/angular/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';

import { WeatherStateService } from './services/weather-state.service';
import { AppState } from './types/weather.types';

import { SearchFormComponent } from './components/search-form.component';
import { LoadingStateComponent } from './components/loading-state.component';
Expand All @@ -15,12 +11,12 @@ import { WeatherContentComponent } from './components/weather-content.component'
selector: 'app-root',
standalone: true,
imports: [
CommonModule,
SearchFormComponent,
LoadingStateComponent,
ErrorStateComponent,
WeatherContentComponent
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<header class="header">
<div class="container">
Expand All @@ -30,22 +26,24 @@ import { WeatherContentComponent } from './components/weather-content.component'

<main class="main">
<div class="container">
@let currentState = state();

<app-search-form
[isLoading]="state.isLoading"
[isLoading]="currentState.isLoading"
(search)="onSearch($event)"
></app-search-form>

<div class="weather-container" data-testid="weather-container">
<app-loading-state [isVisible]="state.isLoading"></app-loading-state>
<app-loading-state [isVisible]="currentState.isLoading"></app-loading-state>

<app-error-state
[isVisible]="!!state.error && !state.isLoading"
[message]="state.error"
[isVisible]="!!currentState.error && !currentState.isLoading"
[message]="currentState.error"
></app-error-state>

<app-weather-content
[isVisible]="!!state.weatherData && !state.isLoading && !state.error"
[weatherData]="state.weatherData"
[isVisible]="!!currentState.weatherData && !currentState.isLoading && !currentState.error"
[weatherData]="currentState.weatherData"
></app-weather-content>
</div>
</div>
Expand All @@ -63,29 +61,10 @@ import { WeatherContentComponent } from './components/weather-content.component'
</footer>
`
})
export class AppComponent implements OnInit, OnDestroy {
state: AppState = {
weatherData: null,
isLoading: false,
error: null
};

private destroy$ = new Subject<void>();

constructor(private weatherStateService: WeatherStateService) {}
export class AppComponent {
private readonly weatherStateService = inject(WeatherStateService);

ngOnInit(): void {
this.weatherStateService.state$
.pipe(takeUntil(this.destroy$))
.subscribe(state => {
this.state = state;
});
}

ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
protected readonly state = this.weatherStateService.state;

onSearch(city: string): void {
this.weatherStateService.loadWeather(city);
Expand Down
104 changes: 79 additions & 25 deletions apps/angular/src/app/components/current-weather.component.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { WeatherData } from '../types/weather.types';
import { WeatherUtils } from '../utils/weather.utils';

@Component({
selector: 'app-current-weather',
standalone: true,
imports: [CommonModule],
imports: [],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<section class="current-section" *ngIf="weatherData">
@if (weatherData(); as weatherData) {
<section class="current-section">
<h2 class="section-title">Current Weather</h2>
<div class="weather-card" data-testid="current-weather">
<div class="current-weather">
<h3 class="current-weather__location" data-testid="current-location">
{{ weatherData.locationName }}{{ weatherData.country ? ', ' + weatherData.country : '' }}
{{ locationLabel() }}
</h3>
<div class="current-weather__main">
<div class="current-weather__icon" data-testid="current-icon">
{{ getWeatherIcon(weatherData.current.weather_code, weatherData.current.is_day) }}
{{ weatherIcon() }}
</div>
<div class="current-weather__temp-group">
<div class="current-weather__temp" data-testid="current-temperature">
{{ formatTemperature(weatherData.current.temperature_2m) }}
{{ currentTemperature() }}
</div>
<div
class="current-weather__condition {{ getConditionClass(weatherData.current.weather_code) }}"
class="current-weather__condition {{ conditionClass() }}"
data-testid="current-condition"
>
{{ getWeatherDescription(weatherData.current.weather_code) }}
{{ weatherDescription() }}
</div>
</div>
</div>
Expand All @@ -36,54 +37,107 @@ import { WeatherUtils } from '../utils/weather.utils';
<div class="weather-detail">
<div class="weather-detail__label">Feels like</div>
<div class="weather-detail__value" data-testid="feels-like">
{{ formatTemperature(weatherData.current.apparent_temperature) }}
{{ apparentTemperature() }}
</div>
</div>
<div class="weather-detail">
<div class="weather-detail__label">Humidity</div>
<div class="weather-detail__value" data-testid="humidity">
{{ formatPercentage(weatherData.current.relative_humidity_2m) }}
{{ humidity() }}
</div>
</div>
<div class="weather-detail">
<div class="weather-detail__label">Wind Speed</div>
<div class="weather-detail__value" data-testid="wind-speed">
{{ formatWindSpeed(weatherData.current.wind_speed_10m) }}
{{ windSpeed() }}
</div>
</div>
<div class="weather-detail">
<div class="weather-detail__label">Pressure</div>
<div class="weather-detail__value" data-testid="pressure">
{{ formatPressure(weatherData.current.pressure_msl) }}
{{ pressure() }}
</div>
</div>
<div class="weather-detail">
<div class="weather-detail__label">Cloud Cover</div>
<div class="weather-detail__value" data-testid="cloud-cover">
{{ formatPercentage(weatherData.current.cloud_cover) }}
{{ cloudCover() }}
</div>
</div>
<div class="weather-detail">
<div class="weather-detail__label">Wind Direction</div>
<div class="weather-detail__value" data-testid="wind-direction">
{{ getWindDirection(weatherData.current.wind_direction_10m) }}
{{ windDirection() }}
</div>
</div>
</div>
</div>
</div>
</section>
</section>
}
`
})
export class CurrentWeatherComponent {
@Input() weatherData: WeatherData | null = null;
readonly weatherData = input<WeatherData | null>(null);

formatTemperature = WeatherUtils.formatTemperature;
formatPercentage = WeatherUtils.formatPercentage;
formatWindSpeed = WeatherUtils.formatWindSpeed;
formatPressure = WeatherUtils.formatPressure;
getWindDirection = WeatherUtils.getWindDirection;
getWeatherDescription = WeatherUtils.getWeatherDescription;
getWeatherIcon = WeatherUtils.getWeatherIcon;
getConditionClass = WeatherUtils.getConditionClass;
readonly locationLabel = computed(() => {
const weatherData = this.weatherData();
if (!weatherData) {
return '';
}

return weatherData.country
? `${weatherData.locationName}, ${weatherData.country}`
: (weatherData.locationName ?? '');
});

readonly weatherIcon = computed(() => {
const current = this.weatherData()?.current;
return current ? WeatherUtils.getWeatherIcon(current.weather_code, current.is_day) : '';
});

readonly currentTemperature = computed(() => {
const current = this.weatherData()?.current;
return current ? WeatherUtils.formatTemperature(current.temperature_2m) : '';
});

readonly conditionClass = computed(() => {
const current = this.weatherData()?.current;
return current ? WeatherUtils.getConditionClass(current.weather_code) : '';
});

readonly weatherDescription = computed(() => {
const current = this.weatherData()?.current;
return current ? WeatherUtils.getWeatherDescription(current.weather_code) : '';
});

readonly apparentTemperature = computed(() => {
const current = this.weatherData()?.current;
return current ? WeatherUtils.formatTemperature(current.apparent_temperature) : '';
});

readonly humidity = computed(() => {
const current = this.weatherData()?.current;
return current ? WeatherUtils.formatPercentage(current.relative_humidity_2m) : '';
});

readonly windSpeed = computed(() => {
const current = this.weatherData()?.current;
return current ? WeatherUtils.formatWindSpeed(current.wind_speed_10m) : '';
});

readonly pressure = computed(() => {
const current = this.weatherData()?.current;
return current ? WeatherUtils.formatPressure(current.pressure_msl) : '';
});

readonly cloudCover = computed(() => {
const current = this.weatherData()?.current;
return current ? WeatherUtils.formatPercentage(current.cloud_cover) : '';
});

readonly windDirection = computed(() => {
const current = this.weatherData()?.current;
return current ? WeatherUtils.getWindDirection(current.wind_direction_10m) : '';
});
}
14 changes: 7 additions & 7 deletions apps/angular/src/app/components/error-state.component.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, input } from '@angular/core';

@Component({
selector: 'app-error-state',
standalone: true,
imports: [CommonModule],
imports: [],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div
class="error"
data-testid="error"
[hidden]="!isVisible"
[hidden]="!isVisible()"
>
<h2 class="error__title">Unable to load weather data</h2>
<p class="error__message">
{{ message || 'Please check the city name and try again.' }}
{{ message() || 'Please check the city name and try again.' }}
</p>
</div>
`
})
export class ErrorStateComponent {
@Input() isVisible = false;
@Input() message: string | null = null;
readonly isVisible = input(false);
readonly message = input<string | null>(null);
}
Loading
Loading