Giter VIP home page Giter VIP logo

frontendmentor-entertainment-app's Introduction

Frontend Mentor - Entertainment web app solution

This is a solution to the Entertainment web app challenge on Frontend Mentor. Frontend Mentor challenges help you improve your coding skills by building realistic project.

Table of contents

Overview

I picked this challenge after a spat of professional boredom and I wanted to knock something out real quick as cleanly as I could. The challenge itself is pretty straightforward but I had two main struggles/learning curves: building a horizontal slider (I've never had to before) and trying to handle all data and events in a reactive fashion instead of an imperative one.

The challenge

Users should be able to:

  • View the optimal layout for the app depending on their device's screen size
  • See hover states for all interactive elements on the page
  • Navigate between Home, Movies, TV Series, and Bookmarked Shows pages
  • Add/Remove bookmarks from all movies and TV series
  • Search for relevant shows on all pages

Screenshot

Desktop Tablet Mobile

Links

My process

For large projects like these I always build a "Kitchen Sink" page with all the design elements on a single page. I try to organize my styles from an Atomic Design perspective (ie: atoms -> elements -> molecules -> pages), but it doesnt always translate well into my projects.

I organize my code into a couple of main "buckets":

  • /pages: contains anything that is routable. These are lazy loaded modules.
  • /shared: contains any reusable components found on (potentially) multiple pages.
  • /styles: contains any global styles, mixes, variables, etc.

My first challenge was the horizontal scroll for the Trending section. I've never had to build one so it was a neat challenge to figure it out. I have a media-container component that renders one or more media-card components. I reused this component for the static grid and for the horizontal scrolling.

Implementing a simple scroll on the X coordinates was a simple bit of CSS, but programmatically scrolling the next set of cards into view was wholly different.

Heres the relevent code from media-container.component.ts:

@ViewChildren('mediaCard', { read: ElementRef })
private mediaCards!: QueryList<ElementRef>;

...

public scrollContainer(scrollToDirection: 'left'|'right') {
    const docWidth = document.documentElement.clientWidth;
    let toLeft = 0;
    let toRight = 0;
    let onScreen = 0;

    this.mediaCards.forEach(card => {
      const cardRect = card.nativeElement.getBoundingClientRect();
      // count how many are off screen to the left
      if (cardRect.left < 0) {
        toLeft++;

      // count how many are on the screen
      } else if (cardRect.right < docWidth) {
        onScreen++;

      // count number of cards to the right
      } else if (cardRect.right > docWidth) {
        toRight++;
      }
    });

    // Get either the next set that'll fit on the screen or the last|first one
    let scrollToIndex = 0;
    const len = this.mediaCards.length;

    if (scrollToDirection === 'right') {
      // Subtract 1 in (onScreen * 2 - 1) so we dont overflow the one slightly overflowing
      scrollToIndex = toRight - onScreen > 0 ? toLeft + (onScreen * 2 - 1) : len - 1;

    } else {
      scrollToIndex = toLeft - onScreen > 0 ? toLeft - onScreen : 0;
    }

    // Scroll into view
    this.mediaCards.get(scrollToIndex)?.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}

Once the "Kitchen Sink" was built out it was a relatively trivial process to integrate all the other pages.

Built with

  • Angular - The only framework worth building large apps with.

What I learned

Reactive programing is/can be very clean. However, Rxjs is not an intuitive library. My goal was not to have any subscriptions in the entire app and to simply pipe events and let async pipes do the rendering. This proved more difficult than I thought.

The search-field component acts as a simple filter for whatever stream of data is on the page. My intent was that the search field would have a simple interface for filtering a stream of media data without a ton of repeated boilerplate code - and without any subscriptions.

I found a solution using only a single subscription in the search-field itself. I had to subscribe to the valueChanges property on the formControl in order to pick up those changes and then emit a value change.

Relevant code:

<!-- home.component.html -->
<app-search-field [searchSubject]="searchString"></app-search-field>
// home.component.ts
public searchString = new BehaviorSubject<string>('');

ngOnInit(): void {
  ...
  this.all = this.dataService.all$;
  this.filteredAll = this.dataService.filterMediaStream(this.searchString, this.all);
}

// search-field.component.ts
public search: FormControl = new FormControl();
private searchSub!: Subscription;

@Input()
public searchSubject!: Subject<string>;

ngOnInit(): void {
  this.searchSub = this.search.valueChanges.pipe(
    debounceTime(250)
  ).subscribe(change => {
    this.searchSubject?.next(change)
  });
}

ngOnDestroy(): void {
  this.searchSub.unsubscribe();
}

// data.service.ts
public filterMediaStream = (searchString: Observable<string>, mediaStream: Observable<IMedia[]>) => {
  return combineLatest([searchString, mediaStream])
    .pipe(
      distinctUntilChanged(),
      map(([search, allMedia]: [string, IMedia[]]) => {
        if (!search) {
          return allMedia;
        }

        return allMedia.filter(media => {
          return new RegExp(search.toLocaleLowerCase()).test(media.title.toLocaleLowerCase())
        });
      })
    );
}

Update On Reactive Programing

I posted a request for feedback on Reddit (gasp! I know, I'm brave). I received a lot of great feedback, especially from @dmitryef who took the time to update things to be purely reactive. The changes were pretty drastic but mostly related to how I was instantiating variables, tracking changes, and updating.

frontendmentor-entertainment-app's People

Contributors

jdillon522 avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.