اذهب إلى المحتوى

نشر مدونة مبنية عبر Angular على Firebase


أسامة دمراني

تعلمنا في الأجزاء السابقة من هذه السلسلة كيفية إنشاء مدونة وإضافة التدوينات وعرضها وحذف كل تدوينة والتعديل عليها وكذلك إضافة استيثاق جوجل وأضفنا صلاحيات للمستخدمين للوصول للمدونة، أما هذه المرة سنكمل ما بقي عمل على المدونة ونضيف اللمسات النهائية عليها مثل إضافة الملف الشخصي لكل كاتب ونشر التعليقات ومشاركة التدوينات على الشبكات الاجتماعية وغيرها ثم سننشر المدونة التي طورناها محليًا على الإنترنت باستعمال Firebase.

هذا المقال جزء من سلسلة عن بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore

إضافة الملف الشخصي للكاتب

سنعرض الملف الشخصي للكاتب على الصفحة الرئيسية، فنفِّذ الأمر التالي لإنشاء مكون author-profile:

ng g c components/author-profile

سنعرض صورة للكاتب مع روابط لحساباته الاجتماعية، وستأتي الصورة من التطبيق نفسه، ونضعها داخل مجلد src/assets.

افتح الملف component.html وضع الشيفرة التالية فيه:

<mat-card class="rightpanel-card mat-elevation-z2">
 <mat-card-content>
  <h4 class="rightdivtext">
   Author
  </h4>
 </mat-card-content>
 <mat-card-content>
  <div class="authorimagecontainer">
   <img class="authorimage" mat-card-avatar
src="../../../assets/ankit-sharma.jpg">
   <h5>Ankit Sharma</h5>
  </div>
 </mat-card-content>
 <mat-divider></mat-divider>
 <mat-card-content>
  <h4 class="rightdivtext">
   Follow Me
  </h4>
 </mat-card-content>
 <mat-card-content>
  <a href="https://www.facebook.com/Ankit.Sharma.0709"
target="_blank"><i class="fa fa-facebook-square"
 aria-hidden="true"></i></a>
  <a href="https://twitter.com/ankitsharma_007" target="_blank"><i
class="fa fa-twitter-square"
 aria-hidden="true"></i></a>
  <a href="https://www.linkedin.com/in/ankitsharma-007/"
target="_blank"><i class="fa fa-linkedin-square"
 aria-hidden="true"></i></a>
  <a href="https://github.com/AnkitSharma-007" target="_blank"><i
class="fa fa-github-square"
 aria-hidden="true"></i></a>
 </mat-card-content>
</mat-card>

افتح الملف component.scss وضع تعريفات التنسيق التالية فيه:

.fa-twitter-square {
 color: #55acee;
}

.fa-facebook-square {
 color: #3b5998;
}

.fa-linkedin-square {
 color: #0976b4;
}

.fa-github-square {
 color: #333;
}

.fa {
 font-size: 3em;
 width: 1em;
 margin-top: 5px;
 cursor: pointer;
}

.mat-card-avatar {
 width: 100px;
 height: 100px;
 margin: auto
 padding: 5px;
}

.authorimagecontainer {
 text-align: center;
}

.rightdivtext {
 color: #636467;
 text-transform: uppercase;
 padding: 2px;
}

.rightpanel-card {
 margin-bottom: 15px;
}

إذا أردنا عرض الملف الشخصي للكاتب على الصفحة الرئيسية، نحتاج إلى إضافة AuthorProfileComponent إلى HomeComponent.

افتح الملف home.component.html وحدِّث المحتوى الموجود فيه كما يلي:

<div class="row left-panel">
 <div class="col-md-9">
  <app-blog-card></app-blog-card>
 </div>
 <div class="col-md-3">
  <app-author-profile></app-author-profile>
 </div>
</div>

افتح المتصفح لترى الملف الشخصي للكاتب على الجانب الأيمن من الصفحة الرئيسية، وسيعرض الملف الشخصي صورة الكاتب مع روابط حساباته الاجتماعية:

profile.png

إضافة خيار تمرير إلى أعلى صفحة التدوينة

سنضيف خيارًا للتمرير إلى أعلى صفحة التدوينة عندما يمرر المستخدم الصفحة إلى الأسفل، بحيث يعرض زر Scroll To top، وإذا نقر المستخدم عليه ينقله إلى أعلى الصفحة بتأثير انتقال ناعم وسلس.

نفّذ الأمر التالي لإنشاء مكون التمرير:

ng g c components/Scroller

افتح الملف component.ts واستبدل الشيفرة التالية بالموجودة فيه:

import { Component, HostListener } from '@angular/core';

@Component({
 selector: 'app-scroller',
 templateUrl: './scroller.component.html',
 styleUrls: ['./scroller.component.scss']
})
export class ScrollerComponent {

 showScroller: boolean;
 showScrollerPosition = 100;

 @HostListener('window:scroll')
 checkScroll() {
  const scrollPosition = window.pageYOffset ||
document.documentElement.scrollTop || document.body.scrollTop || 0;

  if (scrollPosition >= this.showScrollerPosition) {
    this.showScroller = true;
  } else {
    this.showScroller = false;
  }
 }

 gotoTop() {
  window.scroll({
   top: 0,
   left: 0,
   behavior: 'smooth'
  });
 }
}

سنعيِّن قيمة المتغير showScrollerPosition لتكون 100، وهي تمثل عدد البكسلات التي يكون الممرِّر مرئيًا بعدها، وسيراقب التابع checkScroll أحداث التمرير باستخدام المزخرِف ‎@HostListener، ونحسب موضع التمرير الحالي للصفحة، فإن كان أكبر من showScrollerPosition فإننا نعرض زر التمرير إلى الأعلى على الصفحة، ويعيّن التابع gotoTop سلوك الممرِّر، بحيث يمرِّر الصفحة إلى الأعلى بتأثير ناعم.

افتح الملف scroller.component.html واستبدل الشيفرة التالية بالموجود فيه:

<div *ngIf="showScroller" (click)="gotoTop()" class="scroll-to-top"><i class="fa fa-angle-up"></i></div>

افتح الملف scroller.component.scss وضع الشيفرة التالية فيه:

.scroll-to-top {
 display: block;
 background: rgba(100, 100, 100, 0.4);
 color: #ffffff;
 bottom: 4%;
 cursor: pointer;
 position: fixed;
 right: 20px;
 z-index: 999;
 font-size: 24px;
 text-align: center;
 width: 45px;
 height: 45px;
 border-radius: 50%;

 .fa {
  font-weight: 900;
 }
}

.scroll-to-top:hover {
 background-color: #b2b2b2;
}

سنضيف الآن ScrollerComponent إلى BlogComponent، فافتح الملف blog.component.html وأضف السطر التالي في نهاية الملف:

<app-scroller></app-scroller>

وهكذا نكون قد أنشأنا ممرِّرًا لصفحة التدوينة بنجاح، وسنرى في القسم التالي عرضًا لذلك الممرِّر مع خاصية التعليقات.

نشر التعليقات على التدوينة

نضيف الآن خاصية أخرى إلى التدوينات، وهي إمكانية نشر التعليقات على كل تدوينة، بحيث يستطيع أي مستخدم سجل دخوله أن يعلِّق على التدوينات الموجودة، ويكون للمستخدم المدير صلاحية حذف أي تعليق. كذلك تُحذف التعليقات على تدوينة ما بمجرد حذف التدوينة نفسها.

إنشاء نموذج التعليق

أنشئ الملف comment.ts وضع الشيفرة التالية فيه:

export class Comments {
 commentId: string;
 blogId: string;
 email: string;
 commentedBy: string;
 content: string;
 commentDate: any;
}

إنشاء خدمة التعليقات

سننشئ خدمة تتولى معالجة العمليات المتعلقة بقاعدة البيانات فيما يخص التعليقات، فنفّذ الأمر التالي لإنشاء هذه الخدمة:

ng g s services/Comment

افتح الملف comment.service.ts وأضف تعليمات الاستيراد التالية في أعلاه:

import { AngularFirestore } from '@angular/fire/firestore';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Comments } from '../models/comment'

والآن، أضف تعريفات التوابع التالية داخل الصنف CommentService:

export class CommentService {

 constructor(private db: AngularFirestore) { }

 saveComment(comment: Comments) {
  const commentData = JSON.parse(JSON.stringify(comment));
  return this.db.collection('comments').add(commentData);
 }

 getAllCommentsForBlog(blogId: string): Observable<Comments[]> {
  const comments = this.db.collection<Comments>('comments',
   ref => ref.where('blogId', '==', blogId).orderBy('commentDate',
'desc')).snapshotChanges().pipe(
    map(actions => {
     return actions.map(
      c => ({
       commentId: c.payload.doc.id,
       ...c.payload.doc.data()
      }));
    }));
  return comments;
 }

 deleteAllCommentForBlog(blogId: string) {
  const commentsToDelete = this.db.collection('comments', ref =>
ref.where('blogId', '==', blogId)).snapshotChanges();
  commentsToDelete.forEach(
   commentList => {
    commentList.forEach(comment => {
     this.db.doc('comments/' + comment.payload.doc.id).delete();
     });
   }
  );
 }

 deleteSingleComment(commentId: string) {
  return this.db.doc('comments/' + commentId).delete();
 }
}

سيقبل التابع saveComment كائنًا من النوع Comments كمعامِل، وسنحلل المعامِل إلى كائن JSON ونضيفه إلى تجميعة comments في قاعدة بياناتنا، فإذا كانت التجميعة موجودة مسبقًا فسيضاف كائن JSON إليها، أما إذا لم تكن موجودة في قاعدة البيانات فسينشئ تابع الإضافة تجميعة ويضيف الكائن الجديد إليها.

سيقبل التابع getAllCommentsForBlog معرِّف التدوينة كمعامِل، وسيستعلم هذا التابع في تجميعة comments ويعيد قائمة بجميع التعليقات المتوافقة مع معرِّف التدوينة الممرر إليه. ويرتب commentDate قائمة التعليقات تنازليًا وفق التاريخ، لضمان عرض التعليق الأخير في قمة التعليقات.

يقبل التابع deleteAllCommentForBlog معرِّف التدوينة كمعامِل له، ويحذف جميع التعليقات من تجميعة comments وفقًا لمعرِّف التدوينة الممرر إليه، أما التابع deleteSingleComment فسيأخذ معرِّف التعليق كمعامِل له، ويحذف تعليقًا واحدًا من تجميعة comments وفقًا لمعرِّف التعليق الذي مُرِّر إليه.

إنشاء مكون التعليق

نفِّذ الأمر التالي لإنشاء مكون التعليقات الذي يعالج التعليقات التي ينشرها المستخدم:

ng g c components/Comments

افتح الملف comments.component.ts وأضف تعليمات الاستيراد التالية في أعلاه:

import { Input, OnDestroy } from '@angular/core';
import { DatePipe } from '@angular/common';
import { AppUser } from 'src/app/models/appuser';
import { Comments } from 'src/app/models/comment';
import { CommentService } from 'src/app/services/comment.service';
import { AuthService } from 'src/app/services/auth.service';
import { SnackbarService } from 'src/app/services/snackbar.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

أضف مزود DataPipe في قسم مزخرِف ‎@component كما يلي:

@Component({
 ...
 providers: [DatePipe]
}

سنحدِّث الآن صنف CommentsComponent كما يلي:

export class CommentsComponent implements OnInit, OnDestroy {

 @Input()
 blogId;

 appUser: AppUser;
 public comments = new Comments();
 commentList: Comments[] = [];
 private unsubscribe$ = new Subject<void>();

 constructor(private datePipe: DatePipe,
  private commentService: CommentService,
  private authService: AuthService,
  private snackBarService: SnackbarService) { }
}

سيقبل هذا المكون معرِّف التدوينة blogId كمُدخَل له، وسنحقن الخدمات في منشئ الصنف، والآن، أضف تعريفات التوابع التالية داخل الصنف CommentsComponent:

ngOnInit() {
 this.authService.appUser$.subscribe(appUser => this.appUser =
appUser);
 this.getAllComments();
}

onCommentPost(commentForm) {
 this.comments.commentDate = this.datePipe.transform(Date.now(), 'MMdd-yyyy HH:mm:ss');
 this.comments.blogId = this.blogId;
 this.commentService.saveComment(this.comments).then(
  commentForm.resetForm()
 );
}

getAllComments() {
 this.commentService.getAllCommentsForBlog(this.blogId)
  .pipe(takeUntil(this.unsubscribe$))
  .subscribe(result => {
   this.commentList = result;
  });
}

deleteComment(commentId) {
 if (confirm('Do you want to delete this comment!!!')) {
  this.commentService.deleteSingleComment(commentId).then(
   () => {
    this.snackBarService.showSnackBar('Comment Deleted
successfully');
   }
 );
 }
}

login() {
 this.authService.login();
}

ngOnDestroy() {
 this.unsubscribe$.next();
 this.unsubscribe$.complete();
}

سنعيّن commentDate داخل التابع onCommentPost ليكون التاريخ الحالي، ونعيّن خاصية blogId الخاصة بكائن التعليق لتكون معرِّف التدوينة التي نُشر عليها التعليق، وسنستدعي التابع saveComment الخاص بالخدمة CommentService ليخزن التعليق في قاعدة البيانات.

كذلك فإن التابع getAllComments سيستدعي getAllCommentsForBlog من الخدمة CommentService ويستخدم blogId كمعامِل، وسيجلب هذا التابع قائمة التعليقات المنشورة على التدوينة.

سيسمح التابع deleteComment لنا بحذف تعليق بعينه، وسيعرض صندوق تأكيد يستدعي التابع deleteSingleComment من الخدمة CommentService إذا أكد المستخدم خيار حذف ذلك التعليق. أما التابع login فسيسمح للمستخدم أن يسجل الدخول إلى التطبيق باستخدام حساب جوجل.

افتح الملف comments.component.html واستبدل الشيفرة التالية بالموجودة فيه:

<ng-template #anonymousUser>
 <mat-card class="comment-card mat-elevation-z2">
  <a (click)="login()">Login with Google</a> to post comments
 </mat-card>
</ng-template>
<mat-card *ngIf="appUser; else anonymousUser" class="comment-card matelevation-z2">
 <mat-card-title>
 LEAVE A REPLY
 </mat-card-title>
 <mat-card-subtitle>
 Your email address will not be published. Required fields are
marked *
 </mat-card-subtitle>
 <mat-card-content>
  <form #commentForm="ngForm" (ngSubmit)="commentForm.form.valid
&& onCommentPost(commentForm)" novalidate>
   <mat-form-field class="full-width">
    <input matInput placeholder="Name" name="commentedBy"
[(ngModel)]="comments.commentedBy"
 #commentedBy="ngModel" required>
    <mat-error *ngIf="commentForm.submitted &&
commentedBy.errors?.required">Name is required</mat-error>
   </mat-form-field>
 <mat-form-field class="full-width">
  <input matInput placeholder="Email" name="email"
[(ngModel)]="comments.email" #email="ngModel" email
 required>
 <mat-error *ngIf="commentForm.submitted &&
email.errors?.required">Email is required</mat-error>
 <mat-error *ngIf="commentForm.submitted &&
email.errors?.email">Invalid email</mat-error>
 </mat-form-field>
 <mat-form-field class="full-width">
  <textarea matInput placeholder="Comment" name="content"
[(ngModel)]="comments.content"
 #content="ngModel" required></textarea>
  <mat-error *ngIf="commentForm.submitted &&
content.errors?.required">Comment is required</mat-error>
 </mat-form-field>
 <mat-card-actions>
  <button type="
 submit" mat-raised-button color="primary">Post
Comment</button>
   </mat-card-actions>
  </form>
 </mat-card-content>
</mat-card>
<mat-card *ngFor="let comment of commentList" class="comment-card matelevation-z2">
 <mat-card-title>
  <div class="comment-card-title">
   <div>
    {{comment.commentedBy}}
   </div>
   <div *ngIf="appUser?.isAdmin">
    <button mat-icon-button matTooltip="Delete comment"
matTooltipPosition="before" color="accent"
 (click)="deleteComment(comment.commentId)">
     <mat-icon>delete</mat-icon>
    </button>
   </div>
  </div>
 </mat-card-title>

 <mat-card-subtitle>{{comment.commentDate | date:'medium'}}</matcard-subtitle>
 <mat-card-content>
  <p>{{comment.content}}</p>
 </mat-card-content>
</mat-card>

تتاح ميزة نشر التعليقات على التدوينة للمستخدمين الذين سجلوا دخولهم فقط، فإذا لم يكن المستخدم مسجلًا دخوله فنعرض رابط Login with Google لنطلب من المستخدم أن يسجل دخوله بحساب جوجل كي يترك تعليقًا.

ونحن نستخدم استمارة قالبية template-driven لالتقاط تعليقات المستخدم، وهي تستدعي التابع onCommentPost عند الإرسال الناجح، وستحتوي الاستمارة على الحقول الثلاثة التالية:

  • Name: هذا الحقل إجباري ويُستخدم لالتقاط اسم الشخص الذي ينشر التعليق.
  • Email: هذا الحقل إجباري ويُستخدم لالتقاط بريد الشخص الذي ينشر التعليق.
  • Comment: هذا الحقل إجباري أيضًا ويُستخدم لالتقاط نص التعليق.

سنعرض التعليقات المنشورة على التدوينة في تخطيط بطاقة باستخدام العنصر <mat-card>، وسنعرض اسم الشخص الذي ينشر التعليق وتاريخ ذلك النشر ونص التعليق نفسه، وتُعرض قائمة التعليقات أسفل استمارة التعليق مباشرة. أما إذا كان المستخدم له صلاحية المدير فسنعرض أيقونة حذف في كل تعليق لتمكينه من حذف التعليق.

افتح الملف comments.component.scss وانسخ الشيفرة التالية فيه:

a:not([href]):not([tabindex]) {
 text-decoration: underline;
 cursor: pointer;
 color: #1565C0;
}
.comment-card-title{
 display: flex;
 justify-content: space-between;
}

.comment-card {
 margin: 10px 0 15px 0;
}

.full-width {
 width: 100%;
}

سنضيف الآن المكون CommentsComponent إلى BlogComponent، فافتح الملف blog.component.html وأضف الشيفرة التالية في نهاية الملف، مباشرة قبل السطر الذي أضفنا فيه مكون الممرِّر من قبل ScrollerComponent:

<mat-divider></mat-divider>
<app-comments [blogId]="postId"></app-comments>

تحديث مكون قائمة التدوينات

سنضيف الآن خاصية حذف جميع التعليقات المرتبطة بتدوينة عند حذف تلك التدوينة، فافتح الملف blogcard.component.ts وأضف تعريفات الاستيراد الخاصة بالخدمة CommentService، وسنحقن الخدمة في المنشئ:

import { CommentService } from 'src/app/services/comment.service';
...
constructor(
 // حقن خدمة
 private commentService: CommentService) { }

نحدِّث الآن تابع الحذف داخل صنف BlogCardComponent، ونستدعي التابع deleteAllCommentForBlog المعرَّف في CommentService، فأضف سطر الشيفرة التالي داخل كتلة التابع delete، مباشرة قبل استدعاء التابع showSnackBar، وذلك لضمان أن جميع التعليقات المرتبطة بتدوينة تُحذف عند حذف التدوينة نفسها.

this.commentService.deleteAllCommentForBlog(postId);

إنشاء فهرس في قاعدة بيانات Firebase

إذا فتحنا المتصفح الآن وانتقلنا إلى صفحة تفاصيل التدوينة سنحصل على خطأ في طرفية المتصفح يقول "ERROR FirebaseError: The query requires an index":

index.png

بما أننا نستخدم شرط where مع معامل تساوي داخل التابع getAllCommentsForBlog، فنحتاج إلى إنشاء فهرس في قاعدة بياناتنا، وهذا الفهرس ضروري كي يعمل استعلامنا، ونستطيع أن نرى أن رسالة الخطأ توفر رابطًا لإنشاء الفهرس كذلك، فإذا نقرنا على ذلك الرابط سننتقل إلى طرفية firebase، كما نرى في لقطة الشاشة التالية:

composite.png

هنا نرى نافذة Create a composite index على الشاشة مع إعدادات الفهرس، فانقر على زر Create index لإنشاء الفهرس، وسيستغرق ذلك بضع دقائق لبنائه، ثم بمجرد تمام إنشائه تتغير حالة الفهرس إلى Enabled:

enabled.png

اختبار التعليقات

افتح المتصفح وسجل الخروج من التطبيق إذا كنت قد سجلت الدخول إليه، وانتقل إلى صفحة أي تدوينة، ثم مرر إلى أسفل الصفحة، يجب أن ترى زر scroll to top على الجانب الأيمن من الصفحة، فإذا نقرت على ذلك الزر ستعود الصفحة إلى الأعلى بتأثير انتقال ناعم، انظر الصورة أدناه:

scroll.png

كذلك تستطيع أن ترى رسالة تطلب منك تسجيل الدخول بحساب جوجل لتتمكن من نشر التعليقات، فسجل الدخول به لترى نموذج نشر التعليق. اكتب تفاصيل التعليق واضغط على زر Post Comment ليُنشر التعليق ويُعرض في بطاقة تحت النموذج مباشرة.

comment.png

ستعرض بطاقة التعليق اسم الشخص الذي كتبه مع تاريخ ووقت نشر التعليق، فإذا سجلت الدخول كمستخدم مدير فسترى كذلك زر Delete Comment في الجانب العلوي الأيمن من بطاقة التعليق.

إضافة خيار مشاركة للتدوينة

سنضيف الآن خاصية مشاركة التدوينة بحيث نوفر خيار المشاركة من خلال الشبكات الاجتماعية والبريد الإلكتروني، ونستخدم لذلك مكتبة ngx-sharebuttons لإضافة خيار النشر، وهي مكتبة مفتوحة المصدر تزودنا بحل جاهز لإضافة أزرار النشر وأيقوناته لأغلب منصات الرسائل والشبكات الاجتماعية.

تثبيت ngx-sharebuttons

نفذ الأمر التالي لتثبيت حزم ngx-share:

npm i -S @ngx-share/core @ngx-share/button @ngx-share/buttons
@angular/cdk

كما سنثبت حزم الأيقونات باستخدام الأمر التالي:

npm i -S @fortawesome/fontawesome-svg-core @fortawesome/angularfontawesome @fortawesome/free-solid-svg-icons @fortawesome/free-brandssvg-icons

استورد السمة الخاصة بأزرار المشاركة إلى التنسيق العام في الملف app/src/style.scss:

@import '~@ngx-share/button/themes/circles/circles-dark-theme';

كذلك، استورد كلًا من ShareButtonsModule و ShareButtonsConfig و HttpClientModule إلى الملف src/app/app.module.ts. سننشئ إعدادات خاصة للوحدة ShareButtonsModule كما يظهر في الشيفرة التالية:

import { ShareButtonsConfig, ShareModule } from '@ngx-share/core';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { HttpClientModule } from '@angular/common/http';

const customConfig: ShareButtonsConfig = {
 twitterAccount: 'ankitsharma_007'
};

@NgModule({
 ...
 imports: [
  ...
  HttpClientModule,
  FontAwesomeModule,
  ShareModule.withConfig(customConfig),
 ],
})

سنعيِّن اسم حساب تويتر twitterAccount الخاص بكاتب التدوينة في الإعدادات المخصصة، وذلك لضمان الإشارة إلى ذلك الحساب في كل مرة ننشر فيها تدوينة على موقع تويتر.

إنشاء مكون social-share

نفذ الأمر التالي لإنشاء مكون social-share:

ng g c components\social-share

افتح الملف social-share.component.html وضع الشيفرة التالية فيه:

<p><strong>Found this article helpful!!! Share this with your Friends</strong></p>

<button mat-fab shareButton="facebook" [style.backgroundColor]="share.prop.facebook.color">
 <fa-icon [icon]="share.prop.facebook.icon" size="lg"></fa-icon>
</button>
<button mat-fab shareButton="twitter" [style.backgroundColor]="share.prop.twitter.color">
 <fa-icon [icon]="share.prop.twitter.icon" size="lg"></fa-icon>
</button>
<button mat-fab shareButton="linkedin" [style.backgroundColor]="share.prop.linkedin.color">
 <fa-icon [icon]="share.prop.linkedin.icon" size="lg"></fa-icon>
</button>
<button mat-fab shareButton="reddit"
[style.backgroundColor]="share.prop.reddit.color">
 <fa-icon [icon]="share.prop.reddit.icon" size="lg"></fa-icon>
</button>
<button mat-fab shareButton="whatsapp" [style.backgroundColor]="share.prop.whatsapp.color">
 <fa-icon [icon]="share.prop.whatsapp.icon" size="lg"></fa-icon>
</button>
<button mat-fab shareButton="telegram" [style.backgroundColor]="share.prop.telegram.color">
 <fa-icon [icon]="share.prop.telegram.icon" size="lg"></fa-icon>
</button>
<button mat-fab shareButton="print" [style.backgroundColor]="share.prop.print.color">
 <fa-icon [icon]="share.prop.print.icon" size="lg"></fa-icon>
</button>
<button mat-fab shareButton="email" [style.backgroundColor]="share.prop.email.color">
 <fa-icon [icon]="share.prop.email.icon" size="lg"></fa-icon>
</button>

سنضيف SocialShareComponent في BlogComponent، فافتح الملف blog.component.html وضع السطر التالي فيه، مباشرة قبل وسم <mat-divider>.

<app-social-share></app-social-share>

إعداد حزمة الأيقونات

تستخدم مكتبة ngx-sharebuttons على المستوى الداخلي مكتبة FortAwesome/angularfontawesome لحزم أيقوناتها، لذا نحتاج إلى إعداد حزم أيقونات fontawesome في SocialShareComponent من أجل استخدام أيقوناتها لأزرار المشاركة.

أنشئ ملفًا جديدًا باسم icons.ts داخل مجلد src، وضع فيه الشيفرة التالية:

import { faTelegramPlane } from '@fortawesome/free-brands-svgicons/faTelegramPlane';
import { faFacebookF } from '@fortawesome/free-brands-svgicons/faFacebookF';
import { faTwitter } from '@fortawesome/free-brands-svgicons/faTwitter';
import { faRedditAlien } from '@fortawesome/free-brands-svgicons/faRedditAlien';
import { faLinkedinIn } from '@fortawesome/free-brands-svgicons/faLinkedinIn';
import { faWhatsapp } from '@fortawesome/free-brands-svgicons/faWhatsapp';
import { faPrint } from '@fortawesome/free-solid-svg-icons/faPrint';
import { faEnvelope } from '@fortawesome/free-solid-svgicons/faEnvelope';

export const iconpack = [
 faFacebookF, faTwitter, faLinkedinIn, faRedditAlien,
 faTelegramPlane, faWhatsapp, faEnvelope, faPrint
];

لقد استوردنا جميع الأيقونات التي سنستخدمها في تطبيقنا من مكتبة fortawesome، وسنصدر تلك الحزمة الآن ليستخدمها المكون.

افتح الملف social-share.component.ts وأضف تعليمة الاستيراد التالية في أعلاه:

import { FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { iconpack } from 'src/icons';
import { ShareService } from '@ngx-share/core';

حدِّث منشئ الصنف SocialShareComponent كما يلي:

constructor(library: FaIconLibrary, public share: ShareService) {
 library.addIcons(...iconpack);
}

افتح الملف social-share.component.scss وأضف تعريفات التنسيق التالية:

button{
 margin: 5px;
}

اختبار أيقونات المشاركة

افتح المتصفح وانتقل إلى صفحة أي تدوينة ومرِّر إلى الأسفل، سترى قائمة من أزرار النشر معروضة فيها، وستحصل على خيارات النشر التي أعددناها من قبل، انظر الصورة التالية:

share.png

إذا نقرت على أي زر من أزرار النشر تلك فستفتح صفحة التطبيق الموافقة لها لتطلب منك تسجيل الدخول، وعند نجاح عملية تسجيل الدخول ستحصل على زر مشاركة للتدوينة، انظر الصورة التالية لترى خيار مشاركة تويتر.

twitter.png

كما ترى فقد تمت الإشارة إلى حساب ‎@ankitsharma_007 (صاحب التدوينة) على تويتر، وخاصية الإشارة إلى الحساب تلك متاحة في تويتر فقط.

نشر التطبيق على Firebase

تتبقى آخر خطوة وهي نشر التطبيق على Firebase، من خلال الخطوات الموضحة أدناه:

الخطوة الأولى، ثبّت أدوات firebase CLI من خلال npm، كما يلي:

npm install -g firebase-tools

الخطوة الثانية، شغّل الأمر التالي لبناء التطبيق بإعدادات الإنتاج:

ng build --prod

سيعين الخيار prod إعدادات البناء إلى هدف الإنتاج، وهو مُعد في إعدادات مساحة العمل بحيث تستفيد جميع عمليات البناء من الربط bundling، وعمليات الإزالة المحدودة للشيفرة الميتة Dead Code -وهي عملية تحذف الشيفرة التي لا تؤثر على نتيجة البرنامج-، وكذلك عمليات حت الأشجار المحدود tree shaking -وهو مصطلح في جافاسكربت لنفس مفهوم إزالة الشيفرة الميتة.

الخطوة الثالثة، افتح نافذة الطرفية داخل مجلد ‎/blogsite/dist، وشغّل الأمر التالي لتسجيل الدخول إلى firebase:

firebase login

سيفتح ذلك نافذة متصفح تطلب تسجيل الدخول إلى firebase باستخدام حساب جوجل. بعد التسجيل، ارجع إلى الطرفية لتنفيذ الأمر التالي.

الخطوة الرابعة، نفّذ الأمر التالي لتهيئة التطبيق:

firebase init

سيبدأ هذا الأمر مشروع firebase، وسيُطلب منك بضعة أسئلة، فأجب عليها كما يلي:

  • Are you ready to proceed?‎ أدخل y
  • ?Which Firebase CLI features do you want to set up for this folder اختر Hosting
  • Please select an option اختر use an existing project
  • Select a default Firebase project for this directory: اختر اسم مشروعك من القائمة.
  • ?What do you want to use as your public directory اختر blogsite.
  • ?Configure as a single-page app (rewrite all urls to /index.html) أدخل y
  • ?File blogsite/index.html already exists. Overwrite أدخل N. ستحصل الآن على رسالة Firebase initialization complete.

الخطوة الخامسة، انشر التطبيق على firebase، من خلال الأمر التالي:

firebase deploy

هذا الأمر سينشر التطبيق على Firebase، وسيعطيك رابط استضافة، فانتقل إليه لترى التطبيق منشورًا هناك، انظر الصورة التالية:

deploy.png

كذلك تستطيع الوصول إلى رابط الاستضافة من لوحة تحكم firebase، فانتقل إلى صفحة Page Overview الخاصة بمشروع Firebase واختر Hosting من قائمة Develop. تستطيع رؤية أسماء النطاقات لتطبيق الويب الخاص بك على اليمين.

خاتمة

وهكذا نكون قد ختمنا في هذا الفصل عملية بناء مدونة متكاملة كتطبيق وحيد الصفحة باستخدام إطار العمل Angular للواجهة الأمامية، وقاعدة بيانات Firestore للواجهة الخلفية، وتصبح بإكمالك لكامل السلسلة قادرًا على بناء مدونتك الشخصية ونشرها على الإنترنت ومشاركة أصدقائك تدويناتك ويمكنك إضافة أي ميزات أو خصائص أخرى غير التي ذكرناها وهنا نترك الباب مفتوحًا لإبدعائك.

ترجمة -وبتصرف- لفصول من كتاب Build a full stack web application using angular and firebase لصاحبه Ankit Sharma.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...