Skip to content

Latest commit

 

History

History
667 lines (506 loc) · 28.3 KB

README.md

File metadata and controls

667 lines (506 loc) · 28.3 KB

Figma Merch Store

This is a solution to the Figma Merch Store" challenge on Frontend Practice.

Design preview for the Figma Merch Store  coding challenge

Table of contents

Overview

The challenge

Code a replication of the Figma Merch Store site, from this frontend-practice project.

Users should be able to:

  • Toggle the dropdown search bar by clicking the search icon, allowing them to conveniently search for products.
  • Filter the list of products by category.
  • See hover states for all interactive elements on the page,
  • Utilize a draggable slider to effortlessly explore featured products within the hero section and also to view product thumbnails while using the mobile viewport
  • Add, remove, and update products to their shopping cart, ensuring a convenient shopping experience and enabling them to review their selections before finalizing a purchase
  • Navigate through a smooth and streamlined checkout process, ensuring efficient completion of their purchase.
  • Select a country and have the currency automatically update, ensuring accurate pricing information aligned with their chosen location.
  • View and interact with all animated elements on the page
  • View the optimal layout for each page depending on their device's screen size
    • Mobile: < 900px
    • Desktop: > 900px

Demo

View live demo here

Features

Animations

Styling

Angular/JavaScript

Draggable Slider using GSAP

JavaScript iconCSS iconGreen Sock

  • Created a custom Angular Structural @Directive to craft an interactive image slider with draggable functionality. The animation was created using GreenSock's Draggable feature.

  • In the /hero component

    dragabble
  • In the /product component (mobile view)

    dragabble
  • draggable-slider.directive.ts

      @Directive({
        selector: '[dragSliderDir]'
      })
    
      export class DraggableSliderDirective {
        draggable!: Draggable;
    
        constructor(private imagesRef: ElementRef) { }
    
        ngAfterViewInit() {
           gsap.registerPlugin(Draggable);
           this.initializeDragabbleSlider();
        }
    
        initializeDragabbleSlider() {
          let content = this.imagesRef.nativeElement;
          let slider = content.parentNode;
    
          this.draggable = new Draggable(content, {
            type: 'x',
            repeat: -1,
            edgeResistance: 2,
            dragResistance: .1,
            bounds: slider,
            paused: true,
            center: false,
            throwProps: true,
            snap: { x: [0, 100] }
          })
        }
      }
  • hero.component.html

      <div class="hero">
        <div class="container">
          <div class="draggable-images"
                dragSliderDir >
            <svg></svg>
            <svg></svg>
            <svg></svg>
            
            ...
            
          </div>
        </div>
        </div>
  • product.component.html

      ...
    
      <div class="product-grid__thumbnails">
          <div class="product-grid__thumbnails--mobile-drag"
                    dragSliderDir>
            <figure *ngFor="let photo of product.productPhotos; 
            let i = index">
                <img [src]="photo"
                    (click)="expandImg(i)"
                        alt="product.name">
            </figure>
    
          ...
    
        </div>
      </div>
      

If anyone knows how to make this draggable slider an infinite loop please let me know

Swap image on hover

Angular iconSass iconCSS iconHTML icon

  • In the /product-list component, a custom @Directive was created to swap /product-card's default cover image to a pattern/image on :hover, using CSS animations, opacity and positioning.

    hover swap
  • hover-img-swap.directive.ts

        @Directive({
          selector: '[hoverImgSwap]',
        })
    
        export class HoverImgSwapDirective {
    
          @HostBinding('class.hoverImgSwap')
          get cssClasses() {
            return true;
          }
        }
  • product-card.component.html

      <div class="product-list__product" hoverImgSwap>
    
          <figure>
    
            <img [src]="product.hoverPatternImg"/>
    
            <img [src]="product.hoverProductImg"/>
    
            <img [src]="product.productPhotos[0]"/>
    
          </figure>
    
          ...
    
      </div>
  • in _animations.scss

      .hoverImgSwap {
    
        figure {
          position: relative;
          border-radius: $border-radius-default;
          border: none;
    
          img {
            transition: border-color 750ms, opacity 750ms;
            border-radius: $border-radius-default;
            width: 100%;
            height: 100%;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            object-fit: cover;
          }
    
          :first-child,
          :nth-child(2) {
            position: absolute;
            border: $border-default;
            opacity: 0;
          }
    
          :nth-child(3) {
            position: absolute;
            opacity: 1;
          }
        }
    
        &:hover {
          figure {
            :first-child {
              opacity: 1;
              width: 100%;
              height: 100%;
            }
          }
    
          :nth-child(2) {
            opacity: 1;
            max-width: 100%;
            max-height: 100%;
            z-index: 2;
          }
    
          :nth-child(3) {
            opacity: 0;
          }
        }
      }

Marquee Animation

JavaScript iconCSS iconGreen Sock

  • In the /footer component, I created a custom re-useable, Angular Structural @Directive, to craft a scrolling <marquee> animation featuring both text and <svg> elements. This animation was achieved using GreenSock for seamless and dynamic motion.

marquee

marquee

marquee

marquee

  • marquee.directive.ts
      @Directive({
      selector: '[marqueeDirective]'
      })
    
      export class MarqueeDirective implements OnInit {
    
        constructor(private elRef: ElementRef, private renderer: Renderer2) { }
    
        ngOnInit(): void {
          this.initializeMarquee();
        }
    
        initializeMarquee(): void {
          let content = this.elRef.nativeElement.childNodes;
    
          gsap.from(content, {
            x: -this.elRef.nativeElement.offsetWidth,
            repeat: -1,
            duration: 15,
            ease: 'linear'
          })
    
          gsap.to(content, {
            x: this.elRef.nativeElement.offsetWidth,
          })
            .totalProgress(-.7)
        }
      }

If anyone knows how to make this marquee an infinite loop please let me know

Custom fonts

Sass iconCSS icon

  • Custom fonts "Whyte" and "Whyte Inktrap".
  • Whyte has smooth and sharp transitions, while Whyte Inktrap has curt yet also curvy ink traps at its joints.
Whyte
body font
Whyte Inktrap
display font

Dropdown search bar

CSS iconHTML icon

dropdown search bar when the icon is clicked.
marquee

Shopping Cart

Angular iconRxJSCSS iconHTML icon

  • Implemented a user-friendly shopping cart.
  • The shopping cart feature allows users to add products to their cart and view their cart on the homepage for a streamlined shopping experience.

cart-screen

Custom currency @Pipe

Angular iconRxJSHTML icon

  • Developed a custom Angular @Pipe for currency conversion, to update product prices based on the selected country.

  • Utilized the CurrencyBeacon API for most current exchange rates.

  • The default currency is USD

  • Country can be selected by using the dropdown on the navigation menu.

  • The @Pipe converts currency amounts into GBP (British Pound), JPY (Japanese Yen), EUR (Euro), or CAD (Canadian Dollar), providing users with accurate and up-to-date pricing information in their preferred currency.

    currency
  • currency-conversion.pipe.ts

      ...
    
      export class CurrencyConversionPipe implements PipeTransform {
      
      ...
      
      transform(amount: number, country: string, rates: any): any {
          switch (country) {
         
          ...
         
          // United Kingdom
          case 'store-uk':
              return formatCurrency(amount * rates['GBP'].exchangeRate, 'en-us', '£', 'GBP', '1.0-0');
          // Japan
          case 'store-jp':
              return formatCurrency(amount * rates['JPY'].exchangeRate, 'en-JP', '¥', 'JPY', '1.0-0');
          // USA or Just browsing
          default:
              return formatCurrency(amount, 'en-US', '$', 'USD', '1.0-0');
          }
        }
      }
  • products.service.ts

      ...
        loadExchangeRates(): Observable<any> {
    
            return this.http.get<any>(`${environment.API_BASE_URL}/rates`, this.httpOptions)
              .pipe(
                map(res =>
                  res
                ),
                shareReplay(),
                catchError((err) => {
                  throw err + 'Request failed:';
                })
              )
          }
      ...
  • @Pipe in the component template

        ...
            <p class="product-list__product--price">
                {{ product.price |  
                    currencyConversion : 
                    selectedCountry : 
                    (exchangeRates$ | async) 
                  }}
            </p>
        ...

Content filtering

Angular iconHTML icon

  • Within the /call-to-action users are able select "LAYERS" or "COMPONENTS" to filter /product-list by category.
  • Products are filtered through a custom Angular @Pipe .
  • filter-by-category.pipe.ts

        ...
          PipeTransform {
    
            transform(products: Product[], category?: string) {
              if (category) {
                return products.filter(product => product.tags[0] === category);
    
              } else {
                return products
              }
            }
          }

Random color generation

Sass iconCSS icon

Brand Colors

brand-colors

  • Upon rendering, one of the brand's colors is randomly chosen as the background-color for the /footer component.
  • A logo is also selected at random, ensuring that it differs in color from the background. Each time a re-render occurs, a fresh combination is generated.

  • A $random color variable was created to use as an accent color in the /reviews and /size-chart components.

  • $random is updated on render.

  • _variables.scss

      $bgColors: (
          $bio-punk,
          $placid-lilac,
          $fiery-glow,
          $sunflower
      );
    
      $key: random(length($bgColors));
    
      $nth: nth($bgColors, $key );
    
      $random: $nth !default;
  • review.component.scss

      .review {
          background-color: rgba($random, .05);
          border: 2px solid rgba($random, .25);
          }

User Reviews/Ratings Component

Angular iconRxJSSass iconHTML icon

  • Upon the initial render of /product component, up to 8 random "user reviews" and star ratings are generated based on product:tag. Each product has a product:tag category of layers or components.

      export class ReviewComponent implements OnInit {
        ...
        reviews$!: Observable<Review[]>;
        averageRating: number = 0;
        averageRatingStars: string = Array(5).fill(`<span>&#9734;</span>`).join(``);
        ...
    
        ngOnInit(): void {
          this.loadReviews();
        }
    
        loadReviews() {
          const reviews$ = this.reviewsService.
          getRandomReviews(this.reviewCategory).pipe(
              tap(ratings => {
                this.updateAverages(ratings);
              })
            );
    
          this.reviews$ = reviews$;
        }
    
        updateAverages(ratings:Review[]) {
          let average = 0;
    
          ratings.forEach((rating: Review) => { average += rating.rating });
    
          average /= ratings.length;
    
          this.averageRating = average;
    
          average = Math.round(average);
    
          this.averageRatingStars = this.getStars(average);
    
          const averages = {
            numberOfReviews: ratings.length,
            averageRating: this.averageRating,
            averageRatingStars: this.averageRatingStars
          }
    
          this.newAverages.emit(averages);
        }
    
    
        getStars(starRating: number): string {
          return Array(starRating).fill(`<span>&#9733;</span>`)
            .concat(Array(5 - starRating).fill(`<span>&#9734;</span>`))
            .join(``);
        }
      }
  • Uses the /reviews/:tag endpoint

        app.get('/reviews/:tag', (req, res) => {
      
        const tag = req.params.tag;
    
        const reviewOptions = [
          ...REVIEWS.filter((review) => review.type == tag),
          ...REVIEWS.filter((review) => review.type == 'generic'),
        ];
      
        let randomRatings = reviewOptions
          .sort(() => 0.5 - Math.random())
          .slice(0, Math.floor(Math.random() * reviewOptions.length));
    
        return res.status(200).json(randomRatings.slice(0, 8));
      });
  • averageRating and averageRatingStars are computed from the reviews$. Afterward, the interface displays averageRatingStars, alongside a numerical averageRating and the total count of "user reviews".

My Process

I enjoyed working on this project it was a nice balance of styling requirements and functional requirements great project to practice with.

Built with

Angular iconRxJSTypeScript iconJavaScriptSass iconCSS iconGreen SockHTML iconBEMNodeExpressAxios BadgeNodemonNetlify iconFigma iconVercel

Continued development

  • dynamic <svg>'s in hero
  • infinite loop dragabble sliders and marquee

What I learned

GSAP

CSS Grid

Angular routing

Set up routing: Set up routing so that users can navigate between pages. used /product/:id /product/:name to route to project page

Custom @Pipe's

  • Developed a custom Angular @Pipe for currency conversion, to update prices based on the selected country.
  • Developed a custom Angular @Pipeto filter /product-list by category(tag) .

Angular @Directive

  • Implemented custom structural directives to enable reusable and scalable animations in the application.
  • These directives were utilized in the footer marquee, product hover image swap, and draggable image slider components.
  • By encapsulating animation logic within directives, I was able to achieve modularity while reducing code duplication.

Angular in-memory-web-api

Display products with data binding

Used Angular's data binding and router params to display the /product-list of /product-card's which route to each /product detail pages.

Stateless Observable Service using RxJs and Angular Services

  • Developed stateless observable services following the principles of MVC/MVVM architecture, strategically minimizing client-side state storage and instead dynamically retrieving data from the server on demand.
  • Implemented this approach seamlessly within components product.service.ts, cart.service.ts, and ratings.service.ts, enhancing efficiency and maintaining a clean separation of concerns.

JSON Proxy server to store and retrieve data

During development I used JSON Proxy server to store and retrieve data, which was replaced with an express/node server and a database for production.

API Integration

For production I built an API using Node and Express, hosted through Vercel, and integrated through RapidAPI.

API Endpoints

/products
  • returns a list of PRODUCTS
/products/search/:searchTerm
  • returns list of PRODUCTS filtered by searchTerm
/products/featured
  • returns list of featured PRODUCTS
/product/:productId
  • returns a product from the PRODUCT list by :productId
/reviews/:tag
  • returns up to 8 random reviews and ratings based on product:tag
/rates

Useful resources

Design Resources & Inspiration

Author