C# Corner Author Posts Analytics With Angular 13 And .NET 6.0

Introduction 

C# Corner provides RSS feeds (maximum 100 posts only) with various data about their authors. We will use one of these RSS feeds to fetch the author's post details and scrape data such as article category, number of views, and number of likes for each post and save to an SQL Server database. In our application, user can enter an author id and get the post data for that author and save to the database. Once the data is populated, we can fetch the data with different LINQ queries using Entity Framework and display the data in Angular 13 application as a Chart. 

I have already created one application in Angular 8 and .NET framework (4.5) and published one article two and half years ago.  

C# Corner Author posts Analytics with Angular 8 

Now there are significant changes happened to Angular and .NET Core became extremely popular, I have decided to re-write above application with Angular 13 and .NET 6.0.  

Recently I have authored an article about Single Page Application (SPA) creation with .NET 6.0 and Angular 13 using latest template available in Visual Studio 2022. I have used the C# Corner Author’s post data to display the category-wise post details as a chart. I have used HtmlAgilityPack library to scrape the data from C# Corner posts (article / blogs / news) 

You can refer to below articles for more information about these topics. 

In this post, we will see how to display each category-wise post details such as post views, post likes and published date. We will also supply a choice to choose an order to display this information by published date, post likes, post views, category, and post types.  

We can see all these steps one by one.  

Create ASP.NET Core and Angular application in Visual Studio 2022 

We can use the Angular and .NET template available in Visual Studio 2022 to create our application. Currently Visual Studio supports Angular 13 version along with .NET 6.0.  

We have chosen the .NET 6.0 framework.  

Application will be created in a few moments.  

We must install the libraries below using NuGet package manger 

  • HtmlAgilityPack   
  • Microsoft.EntityFrameworkCore.SqlServer   
  • Microsoft.EntityFrameworkCore.Tools 

We are using Entity framework in this application. 

We can add database connection string and parallel task counts inside the appsettings.json file. 

appsettings.json 

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "ConnStr": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=AnalyticsDB;Integrated Security=True;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
  },
  "ParallelTasksCount": 20
}

Database connection string will be used by entity framework to connect SQL database and parallel task counts will be used by web scraping parallel foreach code.  

We can create a Feed class inside a Models folder. This class will be used to get required information from C# Corner RSS feeds. 

Feed.cs 

namespace Analytics.NET6Angular13.Models
{
    public class Feed
    {
        public string Link { get; set; }
        public string Title { get; set; }
        public string FeedType { get; set; }
        public string Author { get; set; }
        public string Content { get; set; }
        public DateTime PubDate { get; set; }

        public Feed()
        {
            Link = "";
            Title = "";
            FeedType = "";
            Author = "";
            Content = "";
            PubDate = DateTime.Today;
        }
    }
}

We can create an ArticleMatrix class inside the Models folder. This class will be used to get information for each article/blog once we get it after web scraping.  

ArticleMatrix.cs 

using System.ComponentModel.DataAnnotations.Schema;

namespace Analytics.NET6Angular13.Models
{
    public class ArticleMatrix
    {
        public int Id { get; set; }
        public string? AuthorId { get; set; }
        public string? Author { get; set; }
        public string? Link { get; set; }
        public string? Title { get; set; }
        public string? Type { get; set; }
        public string? Category { get; set; }
        public string? Views { get; set; }
        [Column(TypeName = "decimal(18,4)")]
        public decimal ViewsCount { get; set; }
        public int Likes { get; set; }
        public DateTime PubDate { get; set; }
    }
}

We can create an Authors class inside the Models folder. 

Authors.cs 

namespace Analytics.NET6Angular13.Models
{
    public class Authors
    {
        public string? AuthorId { get; set; }
        public string? Author { get; set; }
        public int Count { get; set; }
    }
}

We can create Category class. 

Category.cs 

namespace Analytics.NET6Angular13.Models
{
    public class Category
    {
        public string? Name { get; set; }
        public int Count { get; set; }
    }
}

We can create our DB context class for Entity framework. 

MyDbContext.cs 

using Microsoft.EntityFrameworkCore;

namespace Analytics.NET6Angular13.Models
{
    public class MyDbContext : DbContext
    {
        public MyDbContext(DbContextOptions<MyDbContext> options)
            : base(options)
        {
        }
        public DbSet<ArticleMatrix>? ArticleMatrices { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
        }
    }
}

We can create our API controller AnalyticsController and add web scraping code inside it. 

AnalyticsController.cs 

using Analytics.NET6Angular13.Models;
using HtmlAgilityPack;
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
using System.Net;
using System.Xml.Linq;

namespace Analytics.NET6Angular13.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class AnalyticsController : ControllerBase
    {
        readonly CultureInfo culture = new("en-US");
        private readonly MyDbContext _dbContext;
        private readonly IConfiguration _configuration;
        private static readonly object _lockObj = new();
        public AnalyticsController(MyDbContext context, IConfiguration configuration)
        {
            _dbContext = context;
            _configuration = configuration;
        }

        [HttpPost]
        [Route("CreatePosts/{authorId}")]
        public async Task<bool> CreatePosts(string authorId)
        {
            try
            {
                XDocument doc = XDocument.Load("https://www.c-sharpcorner.com/members/" + authorId + "/rss");
                if (doc == null)
                {
                    return false;
                }
                var entries = from item in doc.Root.Descendants().First(i => i.Name.LocalName == "channel").Elements().Where(i => i.Name.LocalName == "item")
                              select new Feed
                              {
                                  Content = item.Elements().First(i => i.Name.LocalName == "description").Value,
                                  Link = (item.Elements().First(i => i.Name.LocalName == "link").Value).StartsWith("/") ? "https://www.c-sharpcorner.com" + item.Elements().First(i => i.Name.LocalName == "link").Value : item.Elements().First(i => i.Name.LocalName == "link").Value,
                                  PubDate = Convert.ToDateTime(item.Elements().First(i => i.Name.LocalName == "pubDate").Value, culture),
                                  Title = item.Elements().First(i => i.Name.LocalName == "title").Value,
                                  FeedType = (item.Elements().First(i => i.Name.LocalName == "link").Value).ToLowerInvariant().Contains("blog") ? "Blog" : (item.Elements().First(i => i.Name.LocalName == "link").Value).ToLowerInvariant().Contains("news") ? "News" : "Article",
                                  Author = item.Elements().First(i => i.Name.LocalName == "author").Value
                              };

                List<Feed> feeds = entries.OrderByDescending(o => o.PubDate).ToList();
                string urlAddress = string.Empty;
                List<ArticleMatrix> articleMatrices = new();
                _ = int.TryParse(_configuration["ParallelTasksCount"], out int parallelTasksCount);

                Parallel.ForEach(feeds, new ParallelOptions { MaxDegreeOfParallelism = parallelTasksCount }, feed =>
                {
                    urlAddress = feed.Link;

                    var httpClient = new HttpClient
                    {
                        BaseAddress = new Uri(urlAddress)
                    };
                    var result = httpClient.GetAsync("").Result;

                    string strData = "";

                    if (result.StatusCode == HttpStatusCode.OK)
                    {
                        strData = result.Content.ReadAsStringAsync().Result;

                        HtmlDocument htmlDocument = new();
                        htmlDocument.LoadHtml(strData);

                        ArticleMatrix articleMatrix = new()
                        {
                            AuthorId = authorId,
                            Author = feed.Author,
                            Type = feed.FeedType,
                            Link = feed.Link,
                            Title = feed.Title,
                            PubDate = feed.PubDate
                        };

                        string category = "Videos";
                        if (htmlDocument.GetElementbyId("ImgCategory") != null)
                        {
                            category = htmlDocument.GetElementbyId("ImgCategory").GetAttributeValue("title", "");
                        }

                        articleMatrix.Category = category;

                        var view = htmlDocument.DocumentNode.SelectSingleNode("//span[@id='ViewCounts']");
                        if (view != null)
                        {
                            articleMatrix.Views = view.InnerText;

                            if (articleMatrix.Views.Contains('m'))
                            {
                                articleMatrix.ViewsCount = decimal.Parse(articleMatrix.Views[0..^1]) * 1000000;
                            }
                            else if (articleMatrix.Views.Contains('k'))
                            {
                                articleMatrix.ViewsCount = decimal.Parse(articleMatrix.Views[0..^1]) * 1000;
                            }
                            else
                            {
                                _ = decimal.TryParse(articleMatrix.Views, out decimal viewCount);
                                articleMatrix.ViewsCount = viewCount;
                            }
                        }
                        else
                        {
                            var newsView = htmlDocument.DocumentNode.SelectSingleNode("//span[@id='spanNewsViews']");
                            if (newsView != null)
                            {
                                articleMatrix.Views = newsView.InnerText;

                                if (articleMatrix.Views.Contains('m'))
                                {
                                    articleMatrix.ViewsCount = decimal.Parse(articleMatrix.Views[0..^1]) * 1000000;
                                }
                                else if (articleMatrix.Views.Contains('k'))
                                {
                                    articleMatrix.ViewsCount = decimal.Parse(articleMatrix.Views[0..^1]) * 1000;
                                }
                                else
                                {
                                    _ = decimal.TryParse(articleMatrix.Views, out decimal viewCount);
                                    articleMatrix.ViewsCount = viewCount;
                                }
                            }
                            else
                            {
                                articleMatrix.ViewsCount = 0;
                            }
                        }
                        var like = htmlDocument.DocumentNode.SelectSingleNode("//span[@id='LabelLikeCount']");
                        if (like != null)
                        {
                            _ = int.TryParse(like.InnerText, out int likes);
                            articleMatrix.Likes = likes;
                        }

                        lock (_lockObj)
                        {
                            articleMatrices.Add(articleMatrix);
                        }
                    }
                });

                _dbContext.ArticleMatrices.RemoveRange(_dbContext.ArticleMatrices.Where(x => x.AuthorId == authorId));

                foreach (ArticleMatrix articleMatrix in articleMatrices)
                {
                    if (articleMatrix.Category == "Videos")
                    {
                        articleMatrix.Type = "Video";
                    }
                    articleMatrix.Category = articleMatrix.Category.Replace("&amp;", "&");
                    await _dbContext.ArticleMatrices.AddAsync(articleMatrix);
                }

                await _dbContext.SaveChangesAsync();
                return true;
            }
            catch
            {
                return false;
            }
        }

        [HttpGet]
        [Route("GetAuthors")]
        public IQueryable<Authors> GetAuthors()
        {
            return _dbContext.ArticleMatrices.GroupBy(author => author.AuthorId)
                  .Select(group =>
                        new Authors
                        {
                            AuthorId = group.FirstOrDefault().AuthorId,
                            Author = group.FirstOrDefault().Author,
                            Count = group.Count()
                        })
                  .OrderBy(group => group.Author);
        }

        [HttpGet]
        [Route("GetCategory/{authorId}")]
        public IQueryable<Category> GetCategory(string authorId)
        {
            return from x in _dbContext.ArticleMatrices.Where(x => x.AuthorId == authorId).GroupBy(x => x.Category)
                   select new Category
                   {
                       Name = x.FirstOrDefault().Category,
                       Count = x.Count()
                   };
        }

        [HttpGet]
        [Route("GetAll/{authorId}")]
        public IQueryable<ArticleMatrix> GetAll(string authorId)
        {
            return _dbContext.ArticleMatrices.Where(x => x.AuthorId == authorId).OrderByDescending(x => x.PubDate);
        }
    }
}

Please note that route of the API controller should not carry “api/” prefix. Otherwise, SPA will not work. 

We have added a new API controller now. We must add this controller entry in Angular proxy configuration file inside the context property. Otherwise, API call from Angular will fail. 

proxy.conf.js 

const { env } = require('process');

const target = env.ASPNETCORE_HTTPS_PORT ? `https://localhost:${env.ASPNETCORE_HTTPS_PORT}` :
  env.ASPNETCORE_URLS ? env.ASPNETCORE_URLS.split(';')[0] : 'http://localhost:52649';

const PROXY_CONFIG = [
  {
    context: [
      "/weatherforecast",
      "/analytics"
   ],
    target: target,
    secure: false,
    headers: {
      Connection: 'Keep-Alive'
    }
  }
]

module.exports = PROXY_CONFIG;

Also note that the above context entry is case sensitive. 

Finally, we can make the changes below inside the Program.cs file.  

Program.cs 

using Analytics.NET6Angular13.Models;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
ConfigurationManager configuration = builder.Configuration;

// Add services to the container.

builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<MyDbContext>(options => options.UseSqlServer(configuration.GetConnectionString("ConnStr")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
}

app.UseStaticFiles();
app.UseRouting();


app.MapControllerRoute(
    name: "default",
    pattern: "{controller}/{action=Index}/{id?}");

app.MapFallbackToFile("index.html"); ;

app.Run();

We are using a local database. You must run the migration commands below in Package Manager Console to create database and table. 

PM > add-migration InititalScript  

PM> update-database  

We have completed the backend code. 

Complete the Angular 13 code 

We can run the npm command to install the node modules for Angular application.  

We can open the ClientApp folder in command prompt and install node modules. (If you have not installed node modules, the system will automatically install while running the application for the first time.) 

npm install 

We must install the client libraries below in our Angular application.  

  • chart.js   
  • ng2-charts   
  • bootstrap   
  • font-awesome  

We can use below single npm command to install all these libraries.  

npm install bootstrap chart.js font-awesome ng2-charts   

We can change styles.css file inside the src folder with code changes given below. 

styles.css 

@import "~bootstrap/dist/css/bootstrap.css";
@import "~font-awesome/css/font-awesome.css";

We can change App component class file with code changes given below. 

app.component.ts 

import { HttpClient } from '@angular/common/http';
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { ChartData, ChartEvent, ChartOptions } from 'chart.js';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

export class AppComponent implements OnInit {
  private url!: string;

  constructor(private http: HttpClient, private fb: FormBuilder, @Inject('BASE_URL') baseUrl: string) {
    this.url = baseUrl + 'analytics';
  }

  chartData: ChartData<'pie'> = {
    labels: [],
    datasets: [
      {
        data: [],
      }
    ]
  };

  chartOptions: ChartOptions = {
    responsive: true,
    plugins: {
      title: {
        display: true,
        text: '',
      },
      legend: {
        display: false
      },
    },
  };

  posts: Post[] = [];
  filteredPosts: Post[] = [];
  authors: Author[] = [];
  authorForm!: FormGroup;
  showLoader!: boolean;
  totalPosts!: number;
  categories: string[] = [];
  counts: number[] = [];

  selectedCategory!: string;
  selectedAuthor!: string;
  showDetails!: boolean;
  authorChanged!: boolean;


  ngOnInit(): void {
    this.authorForm = this.fb.group({
      authorId: '',
      chartType: 'pie',
      author: null,
      category: '',
      showLegend: false,
      orderBy: 'pubDate'
    });
    this.showAuthors();
  }

  showAuthors() {
    this.showLoader = true;
    this.http.get<Author[]>(this.url + '/getauthors')
      .subscribe({
        next: (result) => {
          this.authors = result;
          this.showLoader = false;
        },
        error: (err) => {
          console.error(err);
          this.showLoader = false;
        },
        complete: () => console.info('Get authors completed')
      });
  }

  populateData() {
    if (!this.authorForm.value.authorId) {
      alert('Please give a valid Author Id');
      return;
    }
    this.categories = [];
    this.showLoader = true;
    this.clearChart();

    this.http.post(this.url + '/createposts/' + this.authorForm.value.authorId, null)
      .subscribe({
        next: (result) => {
          this.showAuthors();
          if (result == true) {
            alert("Author " + this.authorForm.value.authorId + "'s data successfully populated; \r\nPlease choose this author from dropdown to see full details!");
          }
          else {
            alert('Invalid Author Id');
          }
          this.authorForm.patchValue({
            author: '',
            chartType: 'pie',
            orderBy: 'pubDate',
            category: '',
            showLegend: false
          });
        },
        error: (err) => {
          console.error(err);
          this.authorForm.patchValue({
            author: ''
          });
        },
        complete: () => {
          this.showLoader = false;
          console.info('Populate data completed')
        }
      });
  }

  fillCategory() {
    this.authorChanged = true;
    this.counts = [];
    this.authorForm.patchValue({
      category: ''
    });
    this.totalPosts = 0;
    this.categories = [];
    this.counts = [];
    this.authorForm.patchValue({
      authorId: this.authorForm.value.author.authorId,
    });
    if (!this.authorForm.value.author.authorId) {
      return;
    }
    this.showLoader = true;
    this.http.get<Categroy[]>(this.url + '/getcategory/' + this.authorForm.value.author.authorId)
      .subscribe({
        next: (result) => {
          result.forEach(x => {
            this.totalPosts += x.count;
            this.categories.push(x.name);
            this.counts.push(x.count);
          });
          if (!result || result.length == 0) return;

          this.http.get<Post[]>(this.url + '/getall/' + this.authorForm.value.author.authorId)
            .subscribe({
              next: (result) => {
                this.posts = result;

                this.chartData = {
                  labels: this.categories,
                  datasets: [
                    {
                      data: this.counts,
                    }
                  ]
                };

                this.chartOptions = {
                  responsive: true,
                  plugins: {
                    title: {
                      display: true,
                      text: 'C# Corner Article Categories for : ' + this.authorForm.value.author.author,
                    },
                    legend: {
                      display: this.authorForm.value.showLegend
                    },
                  },
                };
                this.showLoader = false;
                this.authorChanged = false;
              },
              error: (err) => {
                console.error(err);
              },
              complete: () => {
                console.info('Get all data completed');
              }
            });
        },
        error: (err) => {
          console.error(err);
          this.showLoader = false;
          this.authorChanged = false;
        },
        complete: () => {
          console.info('Fill category completed')
        }
      });
  }

  changeLegends() {
    this.chartOptions = {
      responsive: true,
      plugins: {
        title: {
          display: true,
          text: 'C# Corner Article Categories for : ' + this.authorForm.value.author.author,
        },
        legend: {
          display: this.authorForm.value.showLegend
        },
      },
    };
  }

  fillDetails() {
    var cat = this.authorForm.value.category;
    if (!cat || this.authorChanged || !this.posts || this.posts.length <= 0) return;
    if (cat == 'all') {
      this.filteredPosts = this.posts;
    }
    else {
      this.filteredPosts = this.posts.filter(x => x.category == cat);
    }

    switch (this.authorForm.value.orderBy) {
      case 'views': {
        this.filteredPosts.sort((a, b) => b.viewsCount - a.viewsCount);
        break;
      }
      case 'likes': {
        this.filteredPosts.sort((a, b) => b.likes - a.likes);
        break;
      }
      case 'category': {
        this.filteredPosts.sort((a, b) => {
          let fa = a.category.toLowerCase(),
            fb = b.category.toLowerCase();

          if (fa < fb) {
            return -1;
          }
          if (fa > fb) {
            return 1;
          }
          return 0;
        });
        break;
      }
      case 'type': {
        this.filteredPosts.sort((a, b) => {
          let fa = a.type.toLowerCase(),
            fb = b.type.toLowerCase();

          if (fa < fb) {
            return -1;
          }
          if (fa > fb) {
            return 1;
          }
          return 0;
        });
        break;
        break;
      }
      default: {
        this.filteredPosts.sort((a, b) => new Date(b.pubDate).setHours(0, 0, 0, 0) - new Date(a.pubDate).setHours(0, 0, 0, 0));
        break;
      }
    }

    this.selectedCategory = (cat == 'all') ? 'All' : cat;
    this, this.selectedAuthor = this.authorForm.value.author.author;
    this.showDetails = true;
  }

  clearChart() {
    this.chartData = {
      labels: [],
      datasets: [
        {
          data: [],
        }
      ]
    };
    this.chartOptions = {
      responsive: true,
      plugins: {
        title: {
          display: true,
          text: '',
        },
        legend: {
          display: false
        },
      },
    };
  }

  categorySelected() {
    this.fillDetails();
  }

  back() {
    this.showDetails = false;
  }

}

interface Author {
  authorId: string;
  author: string;
  count: number;
}

interface Categroy {
  name: string;
  count: number;
}

interface Post {
  link: string;
  title: string;
  type: string;
  category: string;
  views: string;
  viewsCount: number;
  likes: number;
  pubDate: Date;
}

Change the App component template file with code changes given below. 

app.component.html 

<app-nav-menu></app-nav-menu>
<form novalidate [formGroup]="authorForm">
  <div class="card row card-row" *ngIf="!showDetails">
    <div class="card-header">
      <div class="row">
        <div class="col-md-6">
          <img src="../assets/c-sharpcorner.png" class="logo"> C# Corner Author Analytics wth
          <img width="30" alt="Angular Logo"
               src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
          Angular 13 and .NET 6
        </div>
        <div class="col-md-6 total-author-position">
          <label>Total</label>&nbsp;<label class="total-author-color-change">Authors</label> populated so far :
          {{authors.length}}
        </div>
      </div>
    </div>

    <div class="card-body">
      <div class="row">
        <div class="col-md-6">
          <div class="form-group row mb-4">
            <label class="col-md-3 col-form-label" for="authorId">Author Id</label>
            <div class="col-md-4">
              <input class="form-control" id="authorId" formControlName="authorId" type="text"
                     placeholder="Eg: sarath-lal7" />
            </div>
            <div class="col-md-5">
              <button class="btn btn-primary mr-3" (click)="populateData()">
                Populate Author Data
              </button>
            </div>
          </div>

          <div class="form-group row mb-4">
            <label class="col-md-3 col-form-label" for="authorId">Author Name</label>
            <div class="col-md-4">
              <select class="form-select" formControlName="author" (ngModelChange)="fillCategory()" id="authorId">
                <option value="" disabled>Select an Author</option>
                <option *ngFor="let myauthor of authors" [ngValue]="myauthor">{{myauthor.author}} </option>
              </select>
            </div>
            <label class="col-md-2 col-form-label" for="chartType">Chart Type</label>
            <div class="col-md-3">
              <select id="chartType" class="form-select" formControlName="chartType">
                <option value="pie">Pie</option>
                <option value="doughnut">Doughnut</option>
                <option value="polarArea">Polar Area</option>
                <option value="radar">Radar</option>
                <option value="bar">Bar</option>
                <option value="line">Line</option>
              </select>
            </div>
          </div>

          <div class="form-group row mb-4" *ngIf="categories.length>0 && posts.length>0">
            <b class="col-md-7">
              Total Categories : {{ categories.length}} &nbsp; &nbsp; Total Posts :
              {{totalPosts}}
            </b>
            <div class="col-md-4">
              <div class="form-check">
                <input class="form-check-input" type="checkbox" formControlName="showLegend"
                       (ngModelChange)="changeLegends()">
                <label class="form-check-label" for="flexCheckChecked">
                  Show Chart Legends
                </label>
              </div>
            </div>
          </div>

          <div class="row mb-4" *ngIf="categories.length>0 && posts.length>0">
            <div class="col-md-3">
            </div>
            <div class="card col-md-6" style="margin: 15px; height:100px;">
              <div class="card-header">
                Choose a Category to see post analytics
              </div>
              <div>
                <select formControlName="category" (ngModelChange)="categorySelected()" class="form-select"
                        id="categoryId">
                  <option value="" disabled>Select a Category</option>
                  <option value="all">(All)</option>
                  <option *ngFor="let mycategory of categories" [ngValue]="mycategory">{{mycategory}} </option>
                </select>
              </div>
            </div>
          </div>

        </div>
        <div class="col-md-6">
          <div class="chart-container chart-position" *ngIf="categories.length>0">
            <canvas baseChart [data]="chartData" [type]="authorForm.value.chartType" [options]="chartOptions">
            </canvas>
          </div>
          <div class="file-loader" *ngIf="showLoader">
            <div class="upload-loader">
              <div class="loader"></div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <div class="card row card-row" *ngIf="showDetails && posts.length>0">
    <div class="card-header">
      <div class="row mb-4">
        <div class="col-md-3">
          <label class="form-label">
            Author Name
          </label>
          <label class="form-control border-less">
            {{selectedAuthor}}
          </label>
        </div>
        <div class="col-md-3">
          <label class="form-label">
            Category
          </label>
          <label class="form-control border-less">
            {{selectedCategory}} ({{filteredPosts!.length}})
          </label>
        </div>
        <div class="col-md-2">
          <label class="form-label">
            Change Order By
          </label>
          <select id="orderBy" formControlName="orderBy" (ngModelChange)="fillDetails()" class="form-select">
            <option value="pubDate">Publish Date</option>
            <option value="views">Views</option>
            <option value="likes">Likes</option>
            <option value="category">Category</option>
            <option value="type">Post Type</option>
          </select>
        </div>
        <div class="col-md-4">
          <label class="form-label">
            Choose a Category
          </label>
          <select formControlName="category" (ngModelChange)="categorySelected()" class="form-select" id="categoryId">
            <option value="" disabled>Select a Category</option>
            <option value="all">(All)</option>
            <option *ngFor="let mycategory of categories" [ngValue]="mycategory">{{mycategory}} </option>
          </select>
        </div>
      </div>
      <button (click)="back()" class="btn btn-primary mr-3">Back to Main Page</button>
    </div>

    <div class="card-body">
      <div class="table-responsive" style="max-height:490px; font-size: 12px">
        <table class="table mb-0" *ngIf="filteredPosts && filteredPosts.length>0">
          <thead>
            <tr>
              <th>Sl.No.</th>
              <th>Post Type</th>
              <th>Category</th>
              <th>Title</th>
              <th>Views</th>
              <th>Likes</th>
              <th>Published Date</th>
            </tr>
          </thead>
          <tbody>
            <tr *ngFor="let post of filteredPosts; let i=index">
              <td>{{i+1}}</td>
              <td>{{post.type}}</td>
              <td>{{post.category}}</td>
              <td><a href="{{post.link}}" target="_blank">{{post.title}}</a></td>
              <td>{{post.views}}</td>
              <td>{{post.likes}}</td>
              <td>{{post.pubDate | date: 'dd-MMM-yyyy'}}</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</form>

Currently, we don’t have style sheet file available with App component. We can add a new file.  

app.component.css 

/* Spin Start*/

.file-loader {
  background-color: rgba(0, 0, 0, .5);
  overflow: hidden;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 100000 !important;
}

.upload-loader {
  position: absolute;
  width: 60px;
  height: 60px;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

  .upload-loader .loader {
    border: 5px solid #f3f3f3 !important;
    border-radius: 50%;
    border-top: 5px solid #005eb8 !important;
    width: 100% !important;
    height: 100% !important;
    -webkit-animation: spin 2s linear infinite;
    animation: spin 2s linear infinite;
  }

@-webkit-keyframes spin {
  0% {
    -webkit-transform: rotate(0deg);
  }

  100% {
    -webkit-transform: rotate(360deg);
  }
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(360deg);
  }
}

/* Spin End*/

.border-less {
  border: 0px none;
  background-color: azure;
}

.card-row {
  margin: 20px;
  height: 680px;
}

.card-header {
  background-color: azure;
  font-weight: bold;
}

.total-author-position {
  text-align: right;
}

.total-author-color-change {
  color: blue;
}

.logo {
  width: 30px;
}

.chart-position {
  position: relative;
  height: 19vh;
  width: 38vw
}

We can change the NavMenu component template file with code changes given below. 

nav-menu.component.html 

<footer>
  <nav class="navbar navbar-light bg-
white mt-5 fixed-bottom">
    <div class="navbar-expand m-auto navbar-text">
      Developed with <i class="fa fa-heart"></i> by <b>
        Sarathlal
        Saseendran
      </b>
    </div>
  </nav>
</footer>

Change the style with code change given below. 

nav-menu.component.css 

.fa-heart {
  color: hotpink;
}

Finally, we can change App module with code changes given below. 

app.module.ts 

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

import { AppComponent } from './app.component';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import { HomeComponent } from './home/home.component';
import { CounterComponent } from './counter/counter.component';
import { FetchDataComponent } from './fetch-data/fetch-data.component';
import { NgChartsModule } from 'ng2-charts';

@NgModule({
  declarations: [
    AppComponent,
    NavMenuComponent,
    HomeComponent,
    CounterComponent,
    FetchDataComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
    HttpClientModule,
    FormsModule,
    ReactiveFormsModule,
    NgChartsModule
    //RouterModule.forRoot([
    //  { path: '', component: HomeComponent, pathMatch: 'full' },
    //  { path: 'counter', component: CounterComponent },
    //  { path: 'fetch-data', component: FetchDataComponent },
    //])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

We have completed the entire coding part. We can run the application now. 

We can enter an author id and populate data for that author.  

You must get the correct author id from C# Corner member profile link. 

https://www.c-sharpcorner.com/members/sarath-lal7  

Above is my member profile. Hence sarath-lal7 is my author id.  

After populating author data, you must choose the author's name from the dropdown list.

You can see all the categories and posts count in a chart. Also, you can see the categories in a dropdown list as well.  

You can choose a category from the dropdown and see the full post details about that category. There is an All categories also in the dropdown.  

Please note that, by default all the posts will be sorted by published date. You can choose a different type of order from the dropdown list.  

You can choose Published Date, Views, Likes, Category, and Post Type wise order from the list.  

You can click the Back to Main Page button to go back to the earlier main page.  

You can enter the correct author id of your favorite C# Corner author and get his/her post analytics easily.  

Above are the views-wise post details about C# Corner founder Mr. Mahesh Chand.  

I have deployed this application to Azure as well. Please check this LIVE App.  

Conclusion 

In this post, we have seen all the steps to create an Angular 13 application with .NET 6.0 as an SPA (Single Page Application) with Visual Studio 2022 default template. We have populated C# Corner author post data using web scraping and saved into SQL Server. We have displayed the category-wise post details in several types of charts like Pie, Doughnut, Polar Area, Radar, Bar, and Line. We have also given a way to choose a category and see the post details of selected category. User can see the post details in different sort order like page views count, page likes count, published date, category and post type. I am still working on this application, and I will be adding more features very soon. Please feel free to give your valuable feedback about this project and article.


Similar Articles