تعلمنا في الأجزاء السابقة من هذه السلسلة كيفية إنشاء مدونة وإضافة التدوينات وعرضها وحذف كل تدوينة والتعديل عليها وكذلك إضافة استيثاق جوجل وأضفنا صلاحيات للمستخدمين للوصول للمدونة، أما هذه المرة سنكمل ما بقي عمل على المدونة ونضيف اللمسات النهائية عليها مثل إضافة الملف الشخصي لكل كاتب ونشر التعليقات ومشاركة التدوينات على الشبكات الاجتماعية وغيرها ثم سننشر المدونة التي طورناها محليًا على الإنترنت باستعمال Firebase.
هذا المقال جزء من سلسلة عن بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore
- مقدمة في بناء تطبيقات الويب باستخدام إطار العمل Angular وقاعدة بيانات Firestore
- بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore - إضافة التدوينات وعرضها
- بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore - تعديل التدوينات
- بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore - إضافة الاستثيثاق
- نشر مدونة مبنية عبر Angular على Firebase
إضافة الملف الشخصي للكاتب
سنعرض الملف الشخصي للكاتب على الصفحة الرئيسية، فنفِّذ الأمر التالي لإنشاء مكون 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>
افتح المتصفح لترى الملف الشخصي للكاتب على الجانب الأيمن من الصفحة الرئيسية، وسيعرض الملف الشخصي صورة الكاتب مع روابط حساباته الاجتماعية:
إضافة خيار تمرير إلى أعلى صفحة التدوينة
سنضيف خيارًا للتمرير إلى أعلى صفحة التدوينة عندما يمرر المستخدم الصفحة إلى الأسفل، بحيث يعرض زر 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":
بما أننا نستخدم شرط where
مع معامل تساوي داخل التابع getAllCommentsForBlog
، فنحتاج إلى إنشاء فهرس في قاعدة بياناتنا، وهذا الفهرس ضروري كي يعمل استعلامنا، ونستطيع أن نرى أن رسالة الخطأ توفر رابطًا لإنشاء الفهرس كذلك، فإذا نقرنا على ذلك الرابط سننتقل إلى طرفية firebase، كما نرى في لقطة الشاشة التالية:
هنا نرى نافذة Create a composite index على الشاشة مع إعدادات الفهرس، فانقر على زر Create index لإنشاء الفهرس، وسيستغرق ذلك بضع دقائق لبنائه، ثم بمجرد تمام إنشائه تتغير حالة الفهرس إلى Enabled:
اختبار التعليقات
افتح المتصفح وسجل الخروج من التطبيق إذا كنت قد سجلت الدخول إليه، وانتقل إلى صفحة أي تدوينة، ثم مرر إلى أسفل الصفحة، يجب أن ترى زر scroll to top على الجانب الأيمن من الصفحة، فإذا نقرت على ذلك الزر ستعود الصفحة إلى الأعلى بتأثير انتقال ناعم، انظر الصورة أدناه:
كذلك تستطيع أن ترى رسالة تطلب منك تسجيل الدخول بحساب جوجل لتتمكن من نشر التعليقات، فسجل الدخول به لترى نموذج نشر التعليق. اكتب تفاصيل التعليق واضغط على زر Post Comment ليُنشر التعليق ويُعرض في بطاقة تحت النموذج مباشرة.
ستعرض بطاقة التعليق اسم الشخص الذي كتبه مع تاريخ ووقت نشر التعليق، فإذا سجلت الدخول كمستخدم مدير فسترى كذلك زر 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; }
اختبار أيقونات المشاركة
افتح المتصفح وانتقل إلى صفحة أي تدوينة ومرِّر إلى الأسفل، سترى قائمة من أزرار النشر معروضة فيها، وستحصل على خيارات النشر التي أعددناها من قبل، انظر الصورة التالية:
إذا نقرت على أي زر من أزرار النشر تلك فستفتح صفحة التطبيق الموافقة لها لتطلب منك تسجيل الدخول، وعند نجاح عملية تسجيل الدخول ستحصل على زر مشاركة للتدوينة، انظر الصورة التالية لترى خيار مشاركة تويتر.
كما ترى فقد تمت الإشارة إلى حساب @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، وسيعطيك رابط استضافة، فانتقل إليه لترى التطبيق منشورًا هناك، انظر الصورة التالية:
كذلك تستطيع الوصول إلى رابط الاستضافة من لوحة تحكم firebase، فانتقل إلى صفحة Page Overview الخاصة بمشروع Firebase واختر Hosting من قائمة Develop. تستطيع رؤية أسماء النطاقات لتطبيق الويب الخاص بك على اليمين.
خاتمة
وهكذا نكون قد ختمنا في هذا الفصل عملية بناء مدونة متكاملة كتطبيق وحيد الصفحة باستخدام إطار العمل Angular للواجهة الأمامية، وقاعدة بيانات Firestore للواجهة الخلفية، وتصبح بإكمالك لكامل السلسلة قادرًا على بناء مدونتك الشخصية ونشرها على الإنترنت ومشاركة أصدقائك تدويناتك ويمكنك إضافة أي ميزات أو خصائص أخرى غير التي ذكرناها وهنا نترك الباب مفتوحًا لإبدعائك.
ترجمة -وبتصرف- لفصول من كتاب Build a full stack web application using angular and firebase لصاحبه Ankit Sharma.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.