البحث في الموقع
المحتوى عن 'إطار العمل angular'.
-
تعلمنا في الأجزاء السابقة من هذه السلسلة كيفية إنشاء مدونة وإضافة التدوينات وعرضها وحذف كل تدوينة والتعديل عليها وكذلك إضافة استيثاق جوجل وأضفنا صلاحيات للمستخدمين للوصول للمدونة، أما هذه المرة سنكمل ما بقي عمل على المدونة ونضيف اللمسات النهائية عليها مثل إضافة الملف الشخصي لكل كاتب ونشر التعليقات ومشاركة التدوينات على الشبكات الاجتماعية وغيرها ثم سننشر المدونة التي طورناها محليًا على الإنترنت باستعمال 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. اقرأ أيضًا بناء تطبيق ويب كامل باستخدام Angular ومنصة Firebase ما هي Angular؟ تهيئة بيئة تطبيقات Angular ونشرها على الويب
-
تعلمنا في الجزء الأول من هذا المقال كيفية بناء مدونة كتطبيق وحيد الصفحة باستخدام إطار العمل Angular للواجهة الأمامية، وقاعدة بيانات Firestore، وإضافة محرر للمدونة، ثم إضافة تدوينات جديدة وعرضها على الصفحة الرئيسية، ثم إضافة ميزات للتعديل على التدوينات وحذفها وتنسيق التدوينات في الصفحة الرئيسية وسنكمل في هذا الجزء العمل على المدونة وسنتعلم كيفية إضافة الاستيثاق authentication لتقييد وصول المستخدمين إلى المدونة وإعطاء كل مستخدم صلاحيات مناسبة لإضافة التدونيات والتعديل عليها. هذا المقال جزء من سلسلة عن بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore مقدمة في بناء تطبيقات الويب باستخدام إطار العمل Angular وقاعدة بيانات Firestore. بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore - إضافة التدوينات وعرضها. بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore - تعديل التدوينات. بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore - إضافة الاستثيثاق. نشر مدونة مبنية عبر Angular على Firebase إضافة استيثاق جوجل سنضيف الآن ميزة التسجيل بحساب جوجل إلى التطبيق الخاص بنا، ونحتاج هنا إلى تفعيل خاصية الاستيثاق authentication من طرفية Firebase كما يلي: انتقل إلى صفحة Project Overview الخاصة بمشروع Firebase. في القائمة اليسرى، تحت تبويب Develop، اختر Authentication. انتقل إلى التبويب Sign-in method. اختر Google من القائمة التي تظهر تحته. انقر على زر Enable المقابل لخيار Google، ثم انقر على Save للحفظ. سنهيئ التطبيق الآن ليستخدم استيثاق جوجل من Firebase. إنشاء نموذج AppUser أنشئ الملف appuser.ts وضع الشيفرة التالية فيه: export class AppUser { name: string; email: string; isAdmin: boolean; photoURL: string; } إنشاء خدمة الاستيثاق لإنشاء خدمة تعالج عملية الاستيثاق، نكتب الأمر التالي: ng g s services/auth افتح الملف auth.service.ts وضع تعريفات الاستيراد التالية فيه: import { AppUser } from '../models/appuser'; import { Observable, of } from 'rxjs'; import { AngularFireAuth } from '@angular/fire/auth'; import { ActivatedRoute, Router } from '@angular/router'; import { AngularFirestore } from '@angular/fire/firestore'; import { switchMap } from 'rxjs/operators'; import * as firebase from 'firebase/app'; صرِّح عن الكائن الملاحَظ observable الذي من النوع AppUser في الصنف AuthService كما يلي: appUser$: Observable<AppUser>; ثم احقن الخدمات في المنشئ: constructor( public afAuth: AngularFireAuth, private route: ActivatedRoute, private router: Router, private db: AngularFirestore ) يحصل الكائن الملاحَظ appUser$ على حالة الاستيثاق للمستخدم، فإذا كان قد سجل دخوله سيجلب بيانات المستخدم من قاعدة بيانات Firebase، وإلا يعيد قيمة غير معرَّفة null: this.appUser$ = this.afAuth.authState.pipe( switchMap(user => { if (user) { return this.db.doc<AppUser>(`appusers/${user.uid}`).valueChanges(); } else { return of(null); } }) ); كذلك، سنضيف التابع updateUserData لحفظ بيانات المستخدم في قاعدة بياناتنا عند نجاح تسجيل الدخول، فنخزن الاسم وعنوان البريد ورابط الصورة التي في حساب جوجل لكل مستخدم في قاعدة البيانات. نضيف التابع updateUserData كما يلي: private updateUserData(user) { const userRef = this.db.doc(`appusers/${user.uid}`); const data = { name: user.displayName, email: user.email, photoURL: user.photoURL }; return userRef.set(data, { merge: true }); } ونحن نخزن هذه البيانات لسببين: معرفة المستخدمين الذين سجلوا الدخول إلى تطبيقنا. عرض اسم المستخدم وصورته في شريط التنقل في حالة تسجيل الدخول الناجح. أضف تعريف التابع التالي لتسجيل الدخول والخروج في صنف AuthService كما يلي: async login() { const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl') || this.router.url; localStorage.setItem('returnUrl', returnUrl); const credential = await this.afAuth.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider()); return this.updateUserData(credential.user); } async logout() { await this.afAuth.auth.signOut().then(() => { this.router.navigate(['/']); }); } سنحصل على الرابط returnUrl من الموجه route ثم نخزن قيمته في الذاكرة المحلية، وذلك لضمان عدم فقد القيمة أثناء إعادة توجيه الصفحة أو تحديثها. سيستوثق التابع signInWithPopup من عميل Firebase باستخدام تدفق توثيق OAuth منبثق -popup-based OAuth authentication flow-، فإذا نجحت عملية تسجيل الدخول سيعيد مستخدمًا مسجَّلًا دخوله مع اعتماديات المزود، أما إذا فشل فسيعيد كائن خطأ يحتوي على معلومات حول هذا الخطأ، وسينفذ التابع logout عملية تسجيل الخروج للمستخدم الحالي ويوجهه إلى الصفحة الرئيسية. تحديث AppComponent افتح الملف app.component.ts وأضف تعريفات الاستيراد التالية في بدايته: import { AuthService } from './services/auth.service'; import { Router } from '@angular/router'; ثم احقن الخدمات في المنشئ كما يلي: constructor( private authService: AuthService, private router: Router ) { } سنشترك في الكائن الملاحَظ appUser$ داخل التابع ngOnInit الخاص بالصنف AppComponent كما يلي: this.authService.appUser$.subscribe(user => { if (!user) { return; } else { const returnUrl = localStorage.getItem('returnUrl'); if (!returnUrl) { return; } localStorage.removeItem('returnUrl'); this.router.navigateByUrl(returnUrl); } }); إذا كان المستخدم قد سجل دخوله فسنجلب قيمة الرابط المعاد من الذاكرة المحلية، فإذا كان ذلك الرابط متاحًا يُوجَّه المستخدم إليه، ثم نحذف الرابط من الذاكرة المحلية. إضافة زر تسجيل الدخول في شريط التنقل افتح الملف nav-bar.component.ts وأضف تعريفات الاستيراد التالية في بدايته: import { AuthService } from 'src/app/services/auth.service'; import { AppUser } from 'src/app/models/appuser'; سنصرِّح عن خاصية تحتفظ ببيانات المستخدم، كما سنحقن AuthService في المنشئ، انظر الشيفرة التالية: appUser: AppUser; constructor(private authService: AuthService) {} كما سنشترك في الكائن appUser$ الخاص بالخدمة AuthService ونعيِّن الخاصية appUser، فنفِّذ الواجهة OnInit على صنف NavBarComponent، وأضف السطر التالي إلى التابع ngOnInit: this.authService.appUser$.subscribe(appUser => this.appUser = appUser); كما سنضيف التابع من أجل معالجة تسجيل الدخول والخروج من تطبيقنا داخل الصنف NavBarComponent كما يلي: login() { this.authService.login(); } logout() { this.authService.logout(); } سنحدِّث الآن قالب شريط التنقل، فافتح الملف الملف nav-bar.component.html واستبدل الشيفرة التالية بالموجود فيه: <mat-toolbar class="nav-bar mat-elevation-z2"> <button mat-button [routerLink]='["/"]'> My blog </button> <ng-container *ngIf="appUser"> <button mat-button [routerLinkActive]='["link-active"]' [routerLink]='["/addpost"]'> Add Post </button> </ng-container> <span class="spacer"></span> <ng-template #anonymousUser> <button mat-button (click)="login()">Login with Google</button> </ng-template> <ng-container *ngIf="appUser; else anonymousUser"> <img mat-card-avatar class="user-avatar" src={{appUser.photoURL}}> <button mat-button [matMenuTriggerFor]="menu"> {{appUser.name}}<mat-icon>arrow_drop_down</mat-icon> </button> <mat-menu #menu="matMenu"> <button mat-menu-item (click)=logout()>Logout</button> </mat-menu> </ng-container> </mat-toolbar> إذا كان المستخدم قد سجل الدخول سنعرض كلا من الاسم والصورة اللذين في حساب جوجل له، أما إذا لم يسجل الدخول فسنعرض زر Login with Google على شريط التنقل. سنضيف تعريف التنسيق التالي في الملف nav-bar.component.scss: .user-avatar { height: 40px; width: 40px; border-radius: 50%; flex-shrink: 0; } تحديث نموذج التطبيق استورد AngularFireAuthModule إلى الملف app.module.ts كما يلي: import { AngularFireAuthModule } from '@angular/fire/auth'; @NgModule({ ... imports: [ ... AngularFireAuthModule, ], }) المستخدم ذو صلاحيات التحرير والحذف افتح الملف blog-card.component.ts وأضف تعريفي الاستيراد التاليين: import { AppUser } from 'src/app/models/appuser'; import { AuthService } from 'src/app/services/auth.service'; سنصرِّح عن خاصية في صنف BlogCardComponent -كما في مكون شريط التنقل-، ونحقن AuthService في المنشئ، انظر الشيفرة التالية: appUser: AppUser; constructor( // خدمات أخرى private authService: AuthService) {} سنشترك في الكائن الملاحَظ appUser$ من AuthService ونعيّن الخاصية appUser، فأضف السطر التالي إلى التابع ngOnInit الخاص بالصنف BlogCardComponent. this.authService.appUser$.subscribe(appUser => this.appUser = appUser); ثم افتح الملف blog-card.components.html وأضف توجيه ngIf إلى الوسم <ng-container>، وهو يحتوي أزرار Edit و Delete، وذلك لقصر استخدام هذين الزرين على المستخدمين الذين سجلوا دخولهم. <ng-container *ngIf="appUser"> نحن الآن نسمح بخاصية التعديل والحذف لأي مستخدم قد سجل دخوله، لكن في القسم التالي سنضيف خاصية الترخيص أو الإذن authorization، ثم نحدِّث الشيفرة لتقصر تلك المزايا على المستخدمين المدراء فقط. الحصول على اسم الكاتب بما أننا لا نسمح بإضافة تدوينات من المستخدمين الذين لم يسجلوا دخولهم، فنستطيع التقاط اسم الكاتب لكل تدوينة، ويكون هو نفس اسمه الذي في حساب جوجل الخاص به، فسنعرض هذا الاسم على صفحة التدوينة إلى جانب تاريخ إنشائها. أضف تعريفات الاستيراد التالية إلى الملف blog-card.components.ts: import { AuthService } from 'src/app/services/auth.service'; import { AppUser } from 'src/app/models/appuser'; سنصرِّح عن خاصية لتحتوي بيانات المستخدم، ثم نحقن AuthService في المنشئ الخاص بالمكون BlogEditorComponent. كذلك، سنشترك في الكائن الملاحَظ appUser$ من AuthService، ونعيّن الخاصية appUser، انظر الشيفرة أدناه: appUser: AppUser; constructor( // حقن آخر للخدمات private authService: AuthService) { } ngOnInit() { ... this.authService.appUser$.subscribe(appUser => this.appUser = appUser); } نعيّن خاصية الكاتب للكائن postData أثناء حفظ التدوينة الجديدة، فأضف السطر التالي داخل قسم else من التابع saveBlogPost، مباشرة قبل استدعاء التابع createPost الخاص بالصنف BlogService. this.postData.author = this.appUser.name; أضف السطر التالي في الملف blog.component.html، بعد السطر الذي نربط فيه createdDate داخل الوسم <mat-card-subtitle>. يُستخدم هذا لعرض اسم الكاتب إلى جانب تاريخ إنشاء التدوينة. <i class="fa fa-user" aria-hidden="true"></i> {{postData.author}} تأمين الوجهات سنضيف حاجز استيثاق auth guard إلى التطبيق لتقييد الوصول غير المصرح له إلى وجهات routes بعينها لا يجب على أي مستخدم الوصول لها خصوصًا المستخدم غير المسجل. شغِّل الأمر التالي لإضافة حاجز جديد: ng g g guards/auth --implements CanActivate افتح الملف src/app/guards/auth.guard.ts وأضف تعريفات الاستيراد التالية: import { Router } from '@angular/router'; import { AuthService } from '../services/auth.service'; import { map } from 'rxjs/operators'; أضف منشئًا في الصنف AuthGuard، واحقن Router و AuthService في المنشئ كما يلي: constructor( private router: Router, private authService: AuthService ) { } سنحدِّث التابع canActivate ليعالج الوصول غير المصدَّق له unauthenticated إلى الوجهات فأضف الشيفرة التالية إلى هذا التابع: return this.authService.appUser$.pipe(map(user => { if (user) { return true; } this.router.navigate(['/'], { queryParams: { returnUrl: state.url } }); return false; })); سنشترك في الكائن appUser$ لجلب حالة الاستيثاق authentication state للمستخدم، فإذا سجل المستخدم الدخول نعيد true، أما إذا لم يسجل الدخول فتحدث ثلاثة أشياء: نعيّن معامِل استعلام query parameter يسمى returnUrl على قيمة الرابط الحالي. ننقل المستخدم إلى الصفحة الرئيسية. نعيد false من التابع. إضافة حواجز الوجهة route guards في نموذج التطبيق إذا أردنا تفعيل حاجز الوجة لوجهة بعينها في التطبيق، نحتاج إلى إضافة الخاصية canActivate إلى الوجهة في الملف app.module.ts، وسنؤمن الوجهات الخاصة بإضافة تدوينة جديدة من خلال إضافة الخاصية canActivate. أضف تعريف الاستيراد التالي في الملف app.module.ts: import { AuthGuard } from './guards/auth.guard'; وحدِّث الاتجاه الخاص بالمكون addpost كما يلي: { path: 'addpost', component: BlogEditorComponent, canActivate:[AuthGuard] }, إذا حاول المستخدم أن يصل إلى الوجهة addpost فسيُستدعى الصنف AuthGuard، فإذا أعاد هذا الصنف true يُسمح للمستخدم أن يصل إلى تلك الوجهة لإضافة تدوينة جديدة، أما إذا أعاد false فلا يُسمح له بالوصول إلى تلك الوجهة ويُعاد إلى الصفحة الرئيسية. اختبار عملية الاستيثاق افتح المتصفح لترى أن بطاقة التدوينة لا تحتوي على زري التعديل والحذف، كذلك لا يظهر زر Add post في شريط التنقل، وذلك لأن المستخدم لم يسجل الدخول بعد، لهذا نرى زر Login with Google في الركن العلوي الأيمن من شريط التنقل. إذا ضغطنا على ذلك الزر ستظهر نافذة منبثقة تطلب منا تسجيل الدخول باستخدام حساب جوجل: ستحدِّث الصفحة نفسها بمجرد نجاح تصديق جوجل، ونرى أن الاتجاه الخاص بـ Add post قد ظهر على شريط التنقل، كما ظهر اسم المستخدم الذي سجل الدخول على الشريط، وسيظهر كل من زر Edit و Delete على بطاقة التدوينة، انظر: تطبيق التصاريح سنطبِّق الآن تصريحًا مبنيًا على قاعدة rule-based لموقعنا، وسنعرِّف دورًا -وهو دور المدير admin- بحيث يستطيع المستخدمون المدراء فقط أن يعدِّلوا ويحذفوا التدوينة، ثم سنضيف إمكانية نشر التعليقات على التدوينة لاحقًا، والتي يستطيع المدير حذفها هي أيضًا. إعداد قاعدة بيانات Firebase لدور المدير لقد عرَّفنا الخاصية البوليانية isAdmin في الصنف AppUser، وتُستخدم هذه الخاصية لتعريف دور المدير للمستخدم، وسنضيف -يدويًا- حقلًا جديدًا باسم isAdmin للمستخدمين الذين نريد منحهم صلاحية المدير، وسننفذ التغييرات في تجميعة appusers داخل قاعدة بيانات Firebase. انتقل إلى قاعدة بيانات Firebase وافتح تجميعة appusers التي تخزن سجلًا لجميع المستخدمين الذين سجلوا الدخول إلى تطبيقنا، انظر الصورة أدناه: انقر على زر Add field لتظهر نافذة تطلب تعريف حقل جديد، فاجعل اسم الحقل isAdmin، ونوعه boolean، وقيمته True، ثم انقر على زر Add لإضافته، انظر الصورة أدناه: سيضاف الحقل الجديد إلى التجميعة: عند نجاح عملية تسجيل الدخول سنجلب بيانات المستخدم من تجميعة appusers ونربطها بكائن يكون نوعه هو الصنف AppUser، ثم نستخدم الخاصية isAdmin بعدها لتقييد الدخول للمستخدم. وبما أننا ننشئ تطبيقًا للتوضيح والشرح فقط يحتوي على دور واحد، فإننا نعيّن خاصية isAdmin يدويًا، أما التطبيقات الكبيرة فلا يُنصح فيها بهذا إذ قد نحتاج إلى التعامل مع أدوار كثيرة مختلفة، ففي تلك الحالة ننشئ تطبيقًا منفصلًا لإدارة الأدوار لجميع المستخدمين. إنشاء حاجز admin-auth نفّذ الأمر التالي لإنشاء حاجز جديد باسم AdminAuthGuard: ng g g guards/admin-auth --implements CanActivate ثم افتح الملف src/app/guards/admin-auth.guard.ts وأضف تعليمات الاستيراد التالية في أعلاه: import { AuthService } from '../services/auth.service'; import { map } from 'rxjs/operators'; import { AppUser } from '../models/appuser'; أضف منشئًا واحقن الموجِّه Router والخدمة AuthService فيه كما يظهر في الشيفرة التالية: constructor( private router: Router, private authService: AuthService) { } سنحدِّث التابع canActivate لمعالجة الوصول غير المصرح له إلى الوجهات، وذلك بإضافة الشيفرة التالية: return this.authService.appUser$.pipe(map((user: AppUser) => { if (user && user.isAdmin) { return true; } this.router.navigate(['/'], { queryParams: { returnUrl: state.url } }); return false; })); سنشترك في الكائن appUser$ لجلب حالة التصديق/التصريح للمستخدم، فإذا كان قد سجل دخوله وكانت قيمة الخاصية isAdmin للمستخدم على القيمة true فإننا نعيد القيمة true أيضًا. بالمثل في سلوك الصنف AuthGuard فإذا لم يكن المستخدم قد سجل دخوله، فستحدث هذه الأشياء الثلاثة: نعين معامِل استعلام باسم returnUrl على قيمة الرابط الحالي. ننقل المستخدم إلى الصفحة الرئيسية. نعيد false من التابع. إضافة صلاحيات مدير للتطبيق لقد حدَّثنا المكون BlogCardComponent في القسم السابق ليقيد الوصول إلى أزرار Edit و Delete ويقصره على المستخدمين الذين سجلوا الدخول، أما الآن فسنحدِّث الشيفرة مرة أخرى لتقصر الوصول على المستخدمين المدراء فقط. افتح الملف src/app/components/blog-card.component.html، وحدِّث التوجيه ngIf الذي في الوسم <ng-container>، والذي بدوره يحتوي على الزر Edit والزر Delete -انظر الشيفرة أدناه- وهذا سيقصر إمكانية التعديل والحذف على المستخدمين الذين سجلوا الدخول فقط. <ng-container *ngIf="appUser?.isAdmin"> أضف تعليمة الاستيراد التالية في الملف app.module.ts: import { AdminAuthGuard } from './guards/admin-auth.guard'; حدِّث الاتجاه الخاص بالمكون editpost كما يلي: { path: 'editpost/:id', component: BlogEditorComponent, canActivate:[AdminAuthGuard] }, لقد قصرنا إمكانية التعديل على التدوينة هنا لتكن للمستخدمين المدراء فقط، فحين يحاول المستخدم الوصول إلى اتجاه editpost، فسيُستدعى الصنف AdminAuthGuard، فإذا أعاد هذا الصنف true يُسمح للمستخدم بالوصول إلى الوجهة editpost لتعديل التدوينة، أما إذا أعاد false فلا يُسمح له بالذهاب لتلك الوجهة لتعديل التدوينة. اختبار التصاريح افتح المتصفح وسجل الدخول إلى التطبيق بمستخدم غير مدير، سترى رابط AddPost في شريط التنقل، لكن بطاقة التدوينة لن يكون فيها زري Edit و Delete، وذلك يعني أن المستخدم يستطيع إضافة تدوينة جديدة لكنه لا يستطيع تعديل تدوينة موجودة أو أن يحذفها. انظر الصورة التالية: انتقل إلى قاعدة بيانات Firebase وأضف حقل isAdmin جديد للمستخدم كما سبق شرحه، أو يمكنك تسجيل الخروج وإعادة تسجيل الدخول بمستخدم آخر يكون فيه حقل isAdmin يحمل القيمة true، وهنا تستطيع أن ترى زري Edit و Delete على بطاقة التدوينة. انظر الصورة التالية: تحديث قواعد الأمان لقاعدة بيانات Firebase لقد أنشأنا قاعدة البيانات في وضع الاختبار، وسنحدِّث القاعدة الأمنية لقاعدة البيانات الآن لتسمح بعملية الكتابة للمستخدم المصرَّح له فقط. من القائمة التي على اليسار، اختر Database في قسم Develop، ثم اختر تبويب Rules، وحدِّث قواعد الأمان كما يلي: service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read; allow write: if request.auth.uid != null; } } } ثم انقر على زر Publish لنشر القاعدة، انظر الصورة أدناه: الخاتمة تعلمنا في هذا الجزء من سلسلة بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore كيفية إضافة استيثاق جوجل للتأكد من تقييد وصول المستخدمين إلى مختلف أجزاء التطبيق وإعطاء كل مستخدم صلاحيات محددة بعد تسجيله في قاعدة بيانات التطبيق. أما في الجزء الخامس والأخير من هذه السلسلة، فسنكمل إضافة اللمسات النهائية على المدونة، مثل إضافة الملف الشخصي لكل كاتب ونشر التعليقات ومشاركة التدوينات على الشبكات الاجتماعية وغيرها ثم سنتعلم كيفية نشر المدونة على الإنترنت باستعمال Firebase. ترجمة -وبتصرف- لفصول من كتاب Build a full stack web application using angular and firebase لصاحبه Ankit Sharma. اقرأ أيضًا بناء تطبيق ويب كامل باستخدام Angular ومنصة Firebase تهيئة بيئة تطبيقات Angular ونشرها على الويب