This is a solution to the Figma Merch Store" challenge on Frontend Practice.
-
View live demo of my solution here
-
View store.figma.com
Code a replication of the Figma Merch Store site, from this frontend-practice project.
- 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
- Mobile:
View live demo here
- Draggable Image Slider
- Swap image on hover
- Marquee
- Circle SVG with rotating text and hover animation
-
Created a custom Angular Structural
@Directive
to craft an interactive image slider with draggable functionality. The animation was created using GreenSock'sDraggable
feature. -
In the
/hero
component -
In the
/product
component (mobile view) -
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
-
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-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; } } }
- 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.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 "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 when the icon is clicked.
- 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.
-
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. -
... 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'); } } }
-
... 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> ...
- 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
.
-
... PipeTransform { transform(products: Product[], category?: string) { if (category) { return products.filter(product => product.tags[0] === category); } else { return products } } }
- 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); }
-
Upon the initial render of
/product
component, up to 8 random "user reviews" and star ratings are generated based onproduct:tag
. Each product has aproduct:tag
category of layers or components.export class ReviewComponent implements OnInit { ... reviews$!: Observable<Review[]>; averageRating: number = 0; averageRatingStars: string = Array(5).fill(`<span>☆</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>★</span>`) .concat(Array(5 - starRating).fill(`<span>☆</span>`)) .join(``); } }
-
Uses the
/reviews/:tag
endpointapp.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
andaverageRatingStars
are computed from thereviews$
. Afterward, the interface displaysaverageRatingStars
, alongside a numericalaverageRating
and the total count of "user reviews".
I enjoyed working on this project it was a nice balance of styling requirements and functional requirements great project to practice with.
- dynamic
<svg>
's in hero - infinite loop dragabble sliders and marquee
Set up routing: Set up routing so that users can navigate between pages. used /product/:id
/product/:name
to route to project page
- Developed a custom Angular
@Pipe
for currency conversion, to update prices based on the selected country. - Developed a custom Angular
@Pipe
to filter/product-list
by category(tag) .
- 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.
Used Angular's data binding and router params to display the /product-list
of /product-card
's which route to each /product
detail pages.
- 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
, andratings.service.ts
, enhancing efficiency and maintaining a clean separation of concerns.
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.
For production I built an API using Node and Express, hosted through Vercel, and integrated through RapidAPI.
- returns a list of
PRODUCTS
- returns list of
PRODUCTS
filtered bysearchTerm
- returns list of featured
PRODUCTS
- returns a
product
from thePRODUCT
list by:productId
- returns up to 8 random
reviews
and ratings based onproduct:tag
- returns most recent
exchangeRates
from the CurrencyBeacon API
- Angular Data Sharing Reference
- How to Secure Angular Environment Variables for Use in GitHub Actions
- Create a Shopping Cart Using Angular and Local Storage with PayPal Checkout
- Scrolling Ticker Tape Web Design Tutorial
- The right way to componentize SVGs for your Angular app
- Angular Currency Pipe & Format Currency In Angular with examples - Angular Currency Pipe is one of the bulit in pipe in Angular used to format currency value according to given country code,currency,decimal,locale information.
- Angular CurrencyPipe
- Proxy Server - JSON server to store and retrieve data during development
- Angular in-memory-web-api
- phosphor icons
- :nth-child() pseudo-class
- CSS Grid Generator
- Udemy: Reactive Angular Course (with RxJs, Angular 16) by Angular University - Build Angular 16 Applications in Reactive style with plain RxJs - Patterns, Anti-Patterns, Lightweight State Management
- Build your own API - Youtube video that quickly shows you how to make a profitable API and sell it on the RapidAPI Hub.
- Portfolio - Chanda Abdul
- GitHub - github.com/Chanda-Abdul