פתרון מבחן בית מס׳ 2 - אפליקציית מזג אויר

לאחר תקופת צינון, אני ממשיך עם הצגת פתרונות מטלות בית.

אם פספסתם את המאמר הקודם, אפשר למצוא אותו כאן.

מבחן בית מס׳ 2 - אפליקציית מזג אוויר

juggle

רמת קושי: מתכנת מתחיל

תיאור: צרו אפליקציה שמציגה את מזג האוויר.

השתמשו בAPI של AccuWeather - בשביל לבצע AutoComplete, Current Weather, 5 Day forecast.

דרישות: צרו 2 עמודים שאפשר לנווט בינהם:

  1. עמוד בית - יציג שדה חיפוש שבו אפשר לחפש מזג אוויר לפי מיקום. הקומופננטה תהיה auto completed. מתחתיו, יהיה מזג האוויר לחמשת הימים של המיקום שהוקלד. המיקום יוכל להישמר/להמחק מ״מועדפים״, כאשר תהיה אינדיקציה אם הוא נמצא שם. המיקום הדיפולטיבי הוא ת״א. (בונוס, להשתמש בgeoloaction api)
    • בונוס: להוסיף טוגל של dark/light theme
    • בונוס: להוסיף טוגל של Celsius/Fahrenheit
  2. עמוד מועדפים - יציג את רשימת המועדפים. כל מיקום מועדף יציג את השם שלו ואת המזג האוויר הנוכחי. לחיצה עליו, תנווט לעמוד הבית עם המיקום הנבחר.
  • כל החיפושים יהיו באנגלית.
  • חובה להשתמש בstate managment.

אפשר לראות את הקוד כאן

ביצוע

נתחיל מליצור פרוייקט חדש, אני אוהב לעבוד עם הקונפיגורציות האלה:

ng g weatherApp --style=scss --skipTests=true --routing=true

שימו לב, שהפעלתי את הדגל של Routing, בשביל שיווצר routing NgModule.

אחרי שסיימנו ליצור את הפרוייקט, ניצור 2 קומפוננטות עבור כל אחד מהעמודים, ונתחיל לעצב את עמוד הבית.

נרצה לבנות Header, שיכיל את הניווט של האפליקציה, את החלפת המצבים והטמפרטורה.

ניווט

נתחיל בלהוסיף את הRoutes לקובץ app-routing:

ונמשיך בבניה של הניווט שלהם:

  <nav>
    <a routerLink="/" routerLinkActive="active"
       [routerLinkActiveOptions]="{exact:true}">index</a>
    <a routerLink="/fav" routerLinkActive="active">fav</a>
  </nav>

אני ארצה להוסיף איזשהי אידנקציה שהעמוד באמת נבחר, ובגלל זה הוספתי את RouterLinkActive, שיוסיף את הקלאס active במידה ואני נמצא בתוך העמוד. שימו לב, שאני צריך להגדיר לו לexact:true - שלא יתבלבל עם הroute השני.

מצב חשוך (dark) / מואר (light)

בעזרת css variables, ותיכנון מוקדם, אפשר להשיג את הבונוס הזה יחסית בקלות.

תחילה ניצור משתנים לכל הצבעים שנרצה להשתמש בהם, בתוך הקובץ יעודי, theme.scss:

—GIST

מה שעומד מאחורי זה, שילוב של SCSS Maps ומשתנים של CSS. אפשר לקרוא עוד על זה כאן.

מכאן, נשאר רק לבנות כפתור, את הלחיצה ולהחליף את הצבע של המשתנה.

  mode = Mode.Light;

  switchMode(mode) {
    if (mode === Mode.Light) {
      document.querySelector('body').style.setProperty('--bg-color', '#272727');
      document.querySelector('body').style.setProperty('--text-color', '#f8fafb');
      this.mode = Mode.Dark;
    } else {
      document.querySelector('body').style.setProperty('--bg-color', '#f8fafb');
      document.querySelector('body').style.setProperty('--text-color', '#272727');
      this.mode = Mode.Light;
    }

שימו לב, שהיה אפשר ליצור Class עזרה, שהיה יכול לקרוא את הערכים והמשתנים מקובץ הSCSS, במקום לכתוב את הצבעים ידנית. אפשר לקרוא על זה כאן.

עמוד הבית

דבר ראשון שנרצה, כשהקומפוננטה עולה, זה להשיג את המיקום הגאוגרפי של המשתמש.

  ngOnInit() {
    if (navigator.geolocation) {
      navigator.geolocation.getCurrentPosition((position) => {
        const {latitude, longitude} = position.coords;
        this.appService.getGeoPosition(latitude, longitude).subscribe((data: GeoPositionRes) => {
          this.handleInitPosition(data);
        });
      });
    } else {
      this.appService.getGeoPosition(DEFAULT_LAT, DEFAULT_LNG).subscribe((data: GeoPositionRes) => {
        this.handleInitPosition(data);
      });
    }
  }

נגיד באופן דיפולטיבי את הקורדיננטות של ת״א, שהשגתי כאן ובhook של אנגולר ngOnInit, השתמשתי בapi geolocation להקפיץ למשתמש את ההודעה אם הוא מרשה לאפליקציה לקבל מידע הגאוגרפי שלו. במידה וכן, אני דורס את המידע הדיפולטיבי שהגדרתי.

הדבר הבא, הוא להתחיל להשתמש בקריאות API. נתחיל בלהרשם לaccuweather ונתחבר.

ארצה לבנות service שינהל את כל הקריאות API, הקריאה הראשונה היא תהיה קריאה בשביל להמיר את המידע של הקורדינטות למידע שאוכל להשתמש, locationKey - בשביל Current Conditions. אשתמש בפונקציה גנרית, שתבנה לי את ה HttpParams, ותוסיף את ה API_KEY במקום אחד.

app.service.ts


  getRequest(url, q?) {
    const params = new HttpParams({fromObject: {apikey: API_KEY, q}});
    return this.http.get(url, {params});

  }

  getGeoPosition(lat: number, lng: number): Observable<any> {
    const url = `http://dataservice.accuweather.com/locations/v1/cities/geoposition/search`;
    return this.getRequest(url, `${lat},${lng}`);
  }

  getAutoComplete(key: string): Observable<any> {
    const url = `http://dataservice.accuweather.com/locations/v1/cities/autocomplete`;
    return this.getRequest(url, `${key}`);
  }

  get5DaysOfForecasts(key: string): Observable<any> {
    const url = `http://dataservice.accuweather.com/forecasts/v1/daily/5day/${key}`;
    return this.getRequest(url);
  }


  getCurrentConditions(key: string): Observable<any> {
    const url = `http://dataservice.accuweather.com/currentconditions/v1/${key}`;
    return this.getRequest(url);
  }

השלב הבא, הוא לקבל את התחזית הרלוונטית. ארצה גם לבנות String, שיכיל את השם של העיר שאותה אני מחפש.

index.component.ts

  private handleInitPosition(geoPositionRes: GeoPositionRes) {
    this.cityName = `${geoPositionRes.ParentCity.EnglishName},${geoPositionRes.Country.EnglishName}`;
    this.appService.get5DaysOfForecasts(geoPositionRes.Key).subscribe((fiveDaysForecastData: FiveDaysForecast) => {
      this.headLine = fiveDaysForecastData.Headline.Text;
      this.forecasts = fiveDaysForecastData.DailyForecasts;
    });
  }

נציג את המידע בצורה יפה, נציג אייקון ונפרסר את התאריך עם pipe של אנגולר.

נוסיף input עבור הautocomplete, ונציג גם את התוצאות שחוזרות מהשרת. נוסיף גם כפתור של הוספה / הסרה ממועדפים.

index.component.html

<input type="text" class="autoComplete"
       [ngModel]="autoCompleteValue"
       (input)="autoCompleteInput.next($event.target.value)">

<div class="autocomplete-suggestions" *ngIf="autoCompletedSuggestions">
  <div class="suggestion" *ngFor="let suggestion of autoCompletedSuggestions" (click)="selectSuggestion(suggestion)">
    , 
  </div>
</div>


<div class="title">
  <div class="name"></div>
  <div class="headLine"></div>
</div>

<div class="fav" *ngIf="cityName" (click)="toggleFavorites()"> Favorites</div>


<div class="forecasts">
  <div class="forecast" *ngFor="let forecast of forecasts;">
    <div class="date"></div>
    <div class="phrase"> </div>
    <div class="tempature">  - </div>
    <img src=""/>
  </div>
</div>

שימו לב שאני משתמש ב Pipe בשביל לשלוט בתמונה שתוצג לפי המזג אוויר - אם המספר של האייקון הוא קטן מ10, צריך להוסיף את התו ״0״ בשביל להציג את התמונה.

import { Pipe, PipeTransform } from '@angular/core';

export const IMG_URL = `https://developer.accuweather.com/sites/default/files`;
@Pipe({
  name: 'accuweatherIcon'
})
export class AccuweatherIconPipe implements PipeTransform {


  transform(value: any): any {
    if (value < 10) {
      value = `0${value}`;
    }
    return `${IMG_URL}/${value}-s.png`;
  }

}

ניצור Subject שינהל את הקלדת התווים ונשתמש באופרטורים debounceTime וswitchMap בשביל לוודא שלא נציף את השרת בקריאות מיותרות.

בכל לחיצה על תוצאה, נקח את המידע ונבצע קריאה נוספת עם המידע החדש.

   this.autoCompleteInput
      .pipe(
        filter((data: string) => data.length > 0),
        takeUntil(this.ngUnSubscribe),
        debounceTime(300),
        switchMap((data: string) => {
          return this.appService.getAutoComplete(data);
        })
      )
      .subscribe((suggestions: AutoCompleteSuggestions[]) => {
        this.autoCompletedSuggestions = suggestions;
      });
  }

  selectSuggestion(suggestion: AutoCompleteSuggestions) {
    this.cityName          = `${suggestion.LocalizedName},${suggestion.Country.LocalizedName}`;
    this.autoCompleteValue = this.cityName;
    this.getFiveDays(suggestion.Key);
    this.autoCompletedSuggestions = null;
  }

מועדפים

בתרגיל הזה, ביקשו מאיתנו להשתמש ב state managment. אני חושב שבמקרה הזה ngrx/mobx זה ממש overkill, והייתי מציע להשתמש במשהו פשוט יותר, לדוגמא Observable Store. לא הייתי משתמש בספרייה הזאת בproducation, אלא יוצר סרביס כזה בעצמי - או באמת משתמש בngrx/mobx.

אני אתקין את החבילה מnpm

npm install @codewithdan/observable-store

ואצור סרביס חדש, בשם weather.service.ts

בשביל לדעת אם העיר שלנו נמצאת במועדפים, אצור פונקציית עזר ואשתמש בה כל פעם בשביל לאתחל את המשתנה favState

Index.component.ts

private getFavState(Key: string) {
  const storeState = this.weatherService.get();
  return storeState[Key] ? REMOVE_FAV : ADD_FAV;
}

selectSuggestion(suggestion: AutoCompleteSuggestions) {
  this.favState          = this.getFavState(suggestion.Key);
  this.cityName          = `${suggestion.LocalizedName},${suggestion.Country.LocalizedName}`;
  this.autoCompleteValue = this.cityName;
  this.getFiveDays(suggestion.Key);
  this.autoCompletedSuggestions = null;
}


private handleInitPosition(geoPositionRes: GeoPositionRes) {
  this.favState    = this.getFavState(geoPositionRes.Key);
  this.cityName    = `${geoPositionRes.ParentCity.EnglishName},${geoPositionRes.Country.EnglishName}`;
  this.getFiveDays(geoPositionRes.Key);
}

כמובן, אוסיף את המטודה שאחראית על להוסיף / להסיר את העיר מהמעודפים

toggleFavorites() {
  const faveState = this.getFavState(this.selectedKey);
  const selectedCity = {
    key: this.selectedKey,
    cityName: this.cityName
  };
  if (faveState === ADD_FAV) {
    this.weatherService.add(selectedCity);
  } else {
    this.weatherService.remove(selectedCity);
  }

  this.favState = faveState === ADD_FAV ? REMOVE_FAV : ADD_FAV;
}

נאזין לשינויים של הsubject, ונוסיף את האובייקט למערך שנדפיס בטמפלט

סיכום

עשינו תרגיל שמצריך הכרה של דברים בסיסים כמו Routing, API, RxJS, State Managment. התרגיל הזה בדרגת קושי קלה ומתאים בעיקר למפתחים מתחילים.

אפשר לראות את הקוד כאן ואת התוצאה כאן.

מסכימים? לא מסכימים? יש לכם שאלות? אשמח אם תשאירו תגובה ותגידו לי מה דעתכם.