All files / app/cards cards.component.ts

100% Statements 83/83
100% Branches 40/40
100% Functions 27/27
100% Lines 80/80

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 2151x 1x 1x 1x 1x                                             1x 17x 17x 14x 17x     17x 17x     17x 17x     17x 17x     17x     17x 2x 2x   2x 1x     1x 2x         17x 2x 2x   2x     2x     2x       14x 13x         13x   1x     13x   1x     13x   12x 11x 11x 11x 11x 11x 11x   1x       1x 1x 1x           17x 11x 11x       11x 17x       17x 13x     17x                             17x             17x 21x 21x 16x       1x       45x 38x   7x 2x 2x 1x     6x       39x 39x       39x                   3x 3x       3x 2x 2x         1x       1x       1x      
import { Component, computed, inject, signal, OnInit, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { NgFor } from '@angular/common';
import { MatIcon } from '@angular/material/icon';
import { ApiService, CardsApiResponse, CardPost } from '../common/services/api.service';
 
export interface Card {
  id?: number;
  name: string;
  creditLimit: string;
  annualFee: string;
  cardType: string;
  image: string;
  perks: string[];
  isMostPopular?: boolean;
  applyUrl?: string;
}
 
@Component({
  selector: 'app-m-cards',
  templateUrl: './cards.component.html',
  styleUrls: ['./cards.component.css'],
  imports: [
    MatIcon,
    NgFor,
  ],
})
export class CardsComponent implements OnInit {
  private readonly apiService = inject(ApiService);
  private readonly platformId = inject(PLATFORM_ID);
  private get isBrowser(): boolean { return isPlatformBrowser(this.platformId); }
  readonly showCardMatcher = true;
 
  // Loading state
  readonly isLoading = signal<boolean>(true);
  readonly hasError = signal<boolean>(false);
 
  // Cards data
  readonly #cards = signal<Card[]>([]);
  readonly cards = this.#cards.asReadonly();
 
  // Filter controls
  readonly selectedFilter = signal<string>('All Cards');
  readonly filters = signal<string[]>(['All Cards']); // Dynamic filters from API
 
  // Pagination control
  readonly visibleCount = signal<number>(6);
 
  // Total cards after filter
  readonly filteredTotal = computed<number>(() => {
    const filter = this.selectedFilter();
    const allCards = this.cards();
 
    if (filter === 'All Cards') {
      return allCards.length;
    }
 
    return allCards.filter(card =>
      card.cardType.toLowerCase().includes(filter.toLowerCase())
    ).length;
  });
 
  // Cards to show
  readonly filteredCards = computed<Card[]>(() => {
    const filter = this.selectedFilter();
    const allCards = this.cards();
 
    const result = filter === 'All Cards'
      ? allCards
      : allCards.filter(card =>
        card.cardType.toLowerCase().includes(filter.toLowerCase())
      );
 
    return result.slice(0, this.visibleCount());
  });
 
  ngOnInit(): void {
    if (this.isBrowser) {
      this.loadApis();
    }
  }
 
  private loadApis(): void {
    this.apiService.getSlider().subscribe({
      next: () => {},
      error: (err) => console.error('Slider Error:', err)
    });
 
    this.apiService.getOfferText().subscribe({
      next: () => {},
      error: (err) => console.error('Offer Text Error:', err)
    });
 
    this.apiService.getCards().subscribe({
      next: (response: CardsApiResponse[]) => {
        if (response && response.length > 0) {
          const cardsData = response[0];
          const mappedCards = this.mapApiCardsToCards(cardsData.posts);
          const uniqueTags = this.extractUniqueTagsFromPosts(cardsData.posts);
          this.filters.set(['All Cards', ...uniqueTags]);
          this.#cards.set(mappedCards);
          this.isLoading.set(false);
        } else {
          this.isLoading.set(false);
        }
      },
      error: (err) => {
        console.error('Cards API Error:', err);
        this.hasError.set(true);
        this.isLoading.set(false);
      }
    });
  }
 
  private extractUniqueTagsFromPosts(posts: CardPost[]): string[] {
    const allTags = posts.flatMap(post => post.tags || []);
    const uniqueTags = Array.from(new Set(allTags));
    return uniqueTags.sort();
  }
 
  private mapApiCardsToCards(posts: CardPost[]): Card[] {
    return posts.map(post => {
      const cardType = post.tags && post.tags.length > 0
        ? post.tags[0]
        : 'Standard Card';
 
      const perks = this.getWpText(post.card_features)
        ? this.getWpText(post.card_features).split(',').map(perk => perk.trim())
        : [];
 
      return {
        id: post.id,
        name: this.resolveCardName(post),
        creditLimit: post.card_data?.credit_limit_label || 'As per eligibility',
        annualFee: post.card_data?.card_fee_amount || 'As per card variant',
        cardType: cardType,
        image: post.featured_image || 'assets/images/pnb-mastercard-business.png',
        perks: perks,
        isMostPopular: !!post.card_data?.is_most_popular,
        applyUrl: post.card_data?.button_url
      };
    });
  }
 
  private resolveCardName(post: CardPost): string {
    const candidateValues: unknown[] = [
      post.title,
      (post as unknown as { card_name?: unknown }).card_name,
      (post as unknown as { post_title?: unknown }).post_title,
      (post as unknown as { card_data?: { card_name?: unknown } }).card_data?.card_name,
    ];
 
    for (const candidate of candidateValues) {
      const parsed = this.getWpText(candidate);
      if (parsed) {
        return parsed;
      }
    }
 
    return 'PNB Credit Card';
  }
 
  private getWpText(value: unknown, fallback = ''): string {
    if (typeof value === 'string') {
      return this.cleanText(value);
    }
    if (value && typeof value === 'object' && 'rendered' in value) {
      const rendered = (value as { rendered?: unknown }).rendered;
      if (typeof rendered === 'string') {
        return this.cleanText(rendered);
      }
    }
    return fallback;
  }
 
  private cleanText(value: string): string {
    const stripped = value.replace(/<[^>]*>/g, '').trim();
    return this.decodeHtmlEntities(stripped);
  }
 
  private decodeHtmlEntities(value: string): string {
    return value
      .replace(/&nbsp;/gi, ' ')
      .replace(/&amp;/gi, '&')
      .replace(/&quot;/gi, '"')
      .replace(/&#39;/gi, "'")
      .replace(/&lt;/gi, '<')
      .replace(/&gt;/gi, '>');
  }
 
  setFilter(filter: string): void {
    this.selectedFilter.set(filter);
    this.visibleCount.set(6);
  }
 
  onFilterKeydown(event: KeyboardEvent, filter: string): void {
    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      this.setFilter(filter);
    }
  }
 
  viewAll(): void {
    this.visibleCount.set(this.filteredTotal());
  }
 
  openApplyPage(): void {
    window.open('https://apply.creditcard.pnb.bank.in/', '_blank', 'noopener,noreferrer');
  }
 
  checkPrequalification(): void {
    window.open('https://apply.creditcard.pnb.bank.in:4443/', '_blank', 'noopener,noreferrer');
  }
}