Purpose: Complete component specifications, code examples, and implementation steps for Sprint 5 Knowledge Base frontend development.
Timeline: Week 2 of Sprint 5 (Nov 4-8, 2025)
npm install ngx-markdown marked)npm install ngx-markdown-editor)cd frontend/recruitment-portal
npm install ngx-markdown marked ngx-markdown-editor --save
frontend/recruitment-portal/src/app/
βββ models/
β βββ article.model.ts # Article interface
β βββ article-category.enum.ts # Category enum
β βββ article-rating.model.ts # Rating interface
βββ services/
β βββ kb.service.ts # Knowledge Base API service
β βββ kb-analytics.service.ts # Analytics tracking
βββ components/
β βββ kb/ # Public KB components
β β βββ kb-home/
β β βββ article-list/
β β βββ article-detail/
β β βββ search-results/
β β βββ widgets/
β β βββ rating-widget/
β β βββ bookmark-button/
β β βββ share-buttons/
β β βββ related-articles/
β βββ admin/
β βββ kb/ # Admin KB components
β βββ kb-list/
β βββ kb-editor/
β βββ kb-analytics/
File: frontend/recruitment-portal/src/app/models/article.model.ts
export interface Article {
id: number;
title: string;
slug: string;
summary: string;
content: string; // Markdown
category: ArticleCategory;
author: string;
publishedAt: Date;
updatedAt: Date;
published: boolean;
viewCount: number;
avgRating: number;
ratingCount: number;
readingTime: number; // minutes
tags: string[];
metaDescription: string;
featuredImage?: string;
relatedArticles?: Article[];
}
export interface ArticleRating {
id: number;
articleId: number;
userId: number;
rating: number; // 1-5
createdAt: Date;
}
export interface ArticleBookmark {
id: number;
articleId: number;
userId: number;
createdAt: Date;
}
export interface ArticleSearchResult {
articles: Article[];
totalElements: number;
totalPages: number;
currentPage: number;
}
File: frontend/recruitment-portal/src/app/models/article-category.enum.ts
export enum ArticleCategory {
DETACHERING_INFO = 'DETACHERING_INFO',
BUREAU_COMPARISON = 'BUREAU_COMPARISON',
CV_TIPS = 'CV_TIPS',
MARKET_INFO = 'MARKET_INFO',
FAQ = 'FAQ',
SUCCESS_STORIES = 'SUCCESS_STORIES'
}
export const CATEGORY_LABELS: Record<ArticleCategory, string> = {
[ArticleCategory.DETACHERING_INFO]: 'Over Detachering & Interim',
[ArticleCategory.BUREAU_COMPARISON]: 'InterimPlaza vs Andere Bureaus',
[ArticleCategory.CV_TIPS]: 'CV & Sollicitatietips',
[ArticleCategory.MARKET_INFO]: 'Marktinformatie',
[ArticleCategory.FAQ]: 'Veelgestelde Vragen',
[ArticleCategory.SUCCESS_STORIES]: 'Succesverhalen'
};
export const CATEGORY_ICONS: Record<ArticleCategory, string> = {
[ArticleCategory.DETACHERING_INFO]: 'info-circle',
[ArticleCategory.BUREAU_COMPARISON]: 'balance-scale',
[ArticleCategory.CV_TIPS]: 'file-alt',
[ArticleCategory.MARKET_INFO]: 'chart-line',
[ArticleCategory.FAQ]: 'question-circle',
[ArticleCategory.SUCCESS_STORIES]: 'trophy'
};
File: frontend/recruitment-portal/src/app/services/kb.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from '../../environments/environment';
import { Article, ArticleSearchResult, ArticleRating, ArticleBookmark } from '../models/article.model';
import { ArticleCategory } from '../models/article-category.enum';
@Injectable({
providedIn: 'root'
})
export class KbService {
private http = inject(HttpClient);
private apiUrl = `${environment.apiUrl}/api/kb`;
private adminApiUrl = `${environment.apiUrl}/api/admin/kb`;
// Public APIs
getArticles(page: number = 0, size: number = 10, category?: ArticleCategory): Observable<ArticleSearchResult> {
let params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
if (category) {
params = params.set('category', category);
}
return this.http.get<ArticleSearchResult>(`${this.apiUrl}/articles`, { params });
}
getArticleBySlug(slug: string): Observable<Article> {
return this.http.get<Article>(`${this.apiUrl}/articles/${slug}`);
}
getArticlesByCategory(category: ArticleCategory, page: number = 0, size: number = 10): Observable<ArticleSearchResult> {
const params = new HttpParams()
.set('page', page.toString())
.set('size', size.toString());
return this.http.get<ArticleSearchResult>(`${this.apiUrl}/articles/category/${category}`, { params });
}
searchArticles(query: string, page: number = 0, size: number = 10): Observable<ArticleSearchResult> {
const params = new HttpParams()
.set('q', query)
.set('page', page.toString())
.set('size', size.toString());
return this.http.get<ArticleSearchResult>(`${this.apiUrl}/articles/search`, { params });
}
getCategories(): Observable<{ category: ArticleCategory; count: number }[]> {
return this.http.get<{ category: ArticleCategory; count: number }[]>(`${this.apiUrl}/categories`);
}
trackView(articleId: number): Observable<void> {
return this.http.post<void>(`${this.apiUrl}/articles/${articleId}/view`, {});
}
// Authenticated APIs
rateArticle(articleId: number, rating: number): Observable<ArticleRating> {
return this.http.post<ArticleRating>(`${this.apiUrl}/articles/${articleId}/rate`, { rating });
}
bookmarkArticle(articleId: number): Observable<ArticleBookmark> {
return this.http.post<ArticleBookmark>(`${this.apiUrl}/articles/${articleId}/bookmark`, {});
}
getBookmarks(): Observable<ArticleBookmark[]> {
return this.http.get<ArticleBookmark[]>(`${this.apiUrl}/bookmarks`);
}
removeBookmark(bookmarkId: number): Observable<void> {
return this.http.delete<void>(`${this.apiUrl}/bookmarks/${bookmarkId}`);
}
// Admin APIs
createArticle(article: Partial<Article>): Observable<Article> {
return this.http.post<Article>(`${this.adminApiUrl}/articles`, article);
}
updateArticle(id: number, article: Partial<Article>): Observable<Article> {
return this.http.put<Article>(`${this.adminApiUrl}/articles/${id}`, article);
}
deleteArticle(id: number): Observable<void> {
return this.http.delete<void>(`${this.adminApiUrl}/articles/${id}`);
}
publishArticle(id: number): Observable<Article> {
return this.http.post<Article>(`${this.adminApiUrl}/articles/${id}/publish`, {});
}
getAnalytics(): Observable<any> {
return this.http.get<any>(`${this.adminApiUrl}/analytics`);
}
}
File: frontend/recruitment-portal/src/app/components/kb/kb-home/kb-home.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { KbService } from '../../../services/kb.service';
import { Article } from '../../../models/article.model';
import { ArticleCategory, CATEGORY_LABELS, CATEGORY_ICONS } from '../../../models/article-category.enum';
@Component({
selector: 'app-kb-home',
standalone: true,
imports: [CommonModule, RouterModule, FormsModule],
templateUrl: './kb-home.component.html',
styleUrls: ['./kb-home.component.scss']
})
export class KbHomeComponent implements OnInit {
private kbService = inject(KbService);
private router = inject(Router);
categories: { category: ArticleCategory; label: string; icon: string; count: number }[] = [];
featuredArticles: Article[] = [];
popularArticles: Article[] = [];
searchQuery: string = '';
loading: boolean = true;
error: string | null = null;
ngOnInit(): void {
this.loadCategories();
this.loadFeaturedArticles();
this.loadPopularArticles();
}
loadCategories(): void {
this.kbService.getCategories().subscribe({
next: (data) => {
this.categories = data.map(item => ({
category: item.category,
label: CATEGORY_LABELS[item.category],
icon: CATEGORY_ICONS[item.category],
count: item.count
}));
},
error: (err) => {
console.error('Failed to load categories', err);
this.error = 'Kon categorieΓ«n niet laden.';
}
});
}
loadFeaturedArticles(): void {
this.kbService.getArticles(0, 3).subscribe({
next: (result) => {
this.featuredArticles = result.articles;
},
error: (err) => {
console.error('Failed to load featured articles', err);
}
});
}
loadPopularArticles(): void {
this.kbService.getArticles(0, 5).subscribe({
next: (result) => {
this.popularArticles = result.articles;
this.loading = false;
},
error: (err) => {
console.error('Failed to load popular articles', err);
this.loading = false;
}
});
}
onSearch(): void {
if (this.searchQuery.trim()) {
this.router.navigate(['/kennisbank/zoeken'], { queryParams: { q: this.searchQuery } });
}
}
navigateToCategory(category: ArticleCategory): void {
this.router.navigate(['/kennisbank/categorie', category.toLowerCase()]);
}
}
Template: kb-home.component.html
<div class="kb-home">
<div class="kb-header">
<h1>Kennisbank</h1>
<p class="subtitle">Alles wat je moet weten over interim, detachering, en carrière in IT</p>
<div class="search-bar">
<input
type="text"
[(ngModel)]="searchQuery"
(keyup.enter)="onSearch()"
placeholder="Zoek artikelen..."
class="search-input">
<button (click)="onSearch()" class="btn btn-primary">
<i class="fas fa-search"></i> Zoeken
</button>
</div>
</div>
<div class="categories-grid" *ngIf="!loading">
<div
*ngFor="let cat of categories"
class="category-card"
(click)="navigateToCategory(cat.category)">
<i class="fas fa-{{cat.icon}}"></i>
<h3>{{cat.label}}</h3>
<p class="article-count">{{cat.count}} artikelen</p>
</div>
</div>
<div class="featured-articles" *ngIf="featuredArticles.length > 0">
<h2>Uitgelichte Artikelen</h2>
<div class="articles-grid">
<div *ngFor="let article of featuredArticles" class="article-card">
<img *ngIf="article.featuredImage" [src]="article.featuredImage" [alt]="article.title">
<div class="article-content">
<h3>
<a [routerLink]="['/kennisbank', article.slug]">{{article.title}}</a>
</h3>
<p class="summary">{{article.summary}}</p>
<div class="article-meta">
<span class="reading-time">
<i class="fas fa-clock"></i> {{article.readingTime}} min
</span>
<span class="views">
<i class="fas fa-eye"></i> {{article.viewCount}}
</span>
<span class="rating" *ngIf="article.avgRating > 0">
<i class="fas fa-star"></i> {{article.avgRating | number:'1.1-1'}}
</span>
</div>
</div>
</div>
</div>
</div>
<div class="popular-articles" *ngIf="popularArticles.length > 0">
<h2>Populaire Artikelen</h2>
<ul class="article-list">
<li *ngFor="let article of popularArticles">
<a [routerLink]="['/kennisbank', article.slug]">
<h4>{{article.title}}</h4>
<p>{{article.summary}}</p>
</a>
</li>
</ul>
</div>
<div class="loading-spinner" *ngIf="loading">
<i class="fas fa-spinner fa-spin"></i> Laden...
</div>
<div class="error-message" *ngIf="error">
<i class="fas fa-exclamation-circle"></i> {{error}}
</div>
</div>
Styles: kb-home.component.scss
@import '../../../../styles/shared/buttons';
@import '../../../../styles/shared/cards';
.kb-home {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
.kb-header {
text-align: center;
margin-bottom: 3rem;
h1 {
font-size: 2.5rem;
color: #2c3e50;
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 1.125rem;
color: #7f8c8d;
margin-bottom: 2rem;
}
.search-bar {
display: flex;
gap: 1rem;
max-width: 600px;
margin: 0 auto;
.search-input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 1rem;
&:focus {
outline: none;
border-color: #0d6efd;
}
}
}
}
.categories-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
.category-card {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: translateY(-5px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
i {
font-size: 3rem;
color: #0d6efd;
margin-bottom: 1rem;
}
h3 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
color: #2c3e50;
}
.article-count {
color: #7f8c8d;
font-size: 0.875rem;
}
}
}
.featured-articles {
margin-bottom: 3rem;
h2 {
font-size: 2rem;
margin-bottom: 1.5rem;
color: #2c3e50;
}
.articles-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
.article-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
&:hover {
transform: translateY(-5px);
}
img {
width: 100%;
height: 200px;
object-fit: cover;
}
.article-content {
padding: 1.5rem;
h3 {
margin-bottom: 0.75rem;
a {
color: #2c3e50;
text-decoration: none;
&:hover {
color: #0d6efd;
}
}
}
.summary {
color: #7f8c8d;
margin-bottom: 1rem;
line-height: 1.6;
}
.article-meta {
display: flex;
gap: 1rem;
font-size: 0.875rem;
color: #95a5a6;
span {
display: flex;
align-items: center;
gap: 0.25rem;
i {
color: #bdc3c7;
}
}
.rating {
color: #f39c12;
i {
color: #f39c12;
}
}
}
}
}
}
}
.popular-articles {
h2 {
font-size: 2rem;
margin-bottom: 1.5rem;
color: #2c3e50;
}
.article-list {
list-style: none;
padding: 0;
li {
background: white;
margin-bottom: 1rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
a {
display: block;
padding: 1.5rem;
text-decoration: none;
color: inherit;
transition: background 0.2s ease;
&:hover {
background: #f8f9fa;
}
h4 {
color: #2c3e50;
margin-bottom: 0.5rem;
}
p {
color: #7f8c8d;
font-size: 0.875rem;
margin: 0;
}
}
}
}
}
.loading-spinner,
.error-message {
text-align: center;
padding: 2rem;
font-size: 1.125rem;
}
.error-message {
color: #e74c3c;
}
}
@media (max-width: 768px) {
.kb-home {
padding: 1rem;
.kb-header {
h1 {
font-size: 2rem;
}
.search-bar {
flex-direction: column;
}
}
.categories-grid {
grid-template-columns: 1fr;
}
.featured-articles .articles-grid {
grid-template-columns: 1fr;
}
}
}
File: frontend/recruitment-portal/src/app/components/kb/article-detail/article-detail.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { MarkdownModule } from 'ngx-markdown';
import { KbService } from '../../../services/kb.service';
import { AuthService } from '../../../services/auth.service';
import { Article } from '../../../models/article.model';
import { RatingWidgetComponent } from '../widgets/rating-widget/rating-widget.component';
import { BookmarkButtonComponent } from '../widgets/bookmark-button/bookmark-button.component';
import { ShareButtonsComponent } from '../widgets/share-buttons/share-buttons.component';
import { RelatedArticlesComponent } from '../widgets/related-articles/related-articles.component';
@Component({
selector: 'app-article-detail',
standalone: true,
imports: [
CommonModule,
RouterModule,
MarkdownModule,
RatingWidgetComponent,
BookmarkButtonComponent,
ShareButtonsComponent,
RelatedArticlesComponent
],
templateUrl: './article-detail.component.html',
styleUrls: ['./article-detail.component.scss']
})
export class ArticleDetailComponent implements OnInit {
private route = inject(ActivatedRoute);
private kbService = inject(KbService);
private authService = inject(AuthService);
article: Article | null = null;
loading: boolean = true;
error: string | null = null;
isAuthenticated: boolean = false;
ngOnInit(): void {
this.isAuthenticated = this.authService.isAuthenticated();
this.route.paramMap.subscribe(params => {
const slug = params.get('slug');
if (slug) {
this.loadArticle(slug);
}
});
}
loadArticle(slug: string): void {
this.loading = true;
this.error = null;
this.kbService.getArticleBySlug(slug).subscribe({
next: (article) => {
this.article = article;
this.loading = false;
// Track view
this.kbService.trackView(article.id).subscribe();
// Update page title
document.title = `${article.title} - InterimPlaza Kennisbank`;
},
error: (err) => {
console.error('Failed to load article', err);
this.error = 'Artikel niet gevonden.';
this.loading = false;
}
});
}
onRated(rating: number): void {
if (!this.article || !this.isAuthenticated) return;
this.kbService.rateArticle(this.article.id, rating).subscribe({
next: () => {
console.log('Article rated successfully');
// Reload article to get updated rating
if (this.article) {
this.loadArticle(this.article.slug);
}
},
error: (err) => {
console.error('Failed to rate article', err);
}
});
}
onBookmarked(): void {
if (!this.article || !this.isAuthenticated) return;
this.kbService.bookmarkArticle(this.article.id).subscribe({
next: () => {
console.log('Article bookmarked successfully');
},
error: (err) => {
console.error('Failed to bookmark article', err);
}
});
}
}
Template: article-detail.component.html
<div class="article-detail" *ngIf="!loading && article">
<div class="article-header">
<div class="breadcrumb">
<a routerLink="/kennisbank">Kennisbank</a> /
<span>{{article.category}}</span> /
<span>{{article.title}}</span>
</div>
<h1>{{article.title}}</h1>
<div class="article-meta">
<span class="author">
<i class="fas fa-user"></i> {{article.author}}
</span>
<span class="date">
<i class="fas fa-calendar"></i> {{article.publishedAt | date:'dd MMMM yyyy'}}
</span>
<span class="reading-time">
<i class="fas fa-clock"></i> {{article.readingTime}} min leestijd
</span>
<span class="views">
<i class="fas fa-eye"></i> {{article.viewCount}} weergaven
</span>
</div>
<img *ngIf="article.featuredImage"
[src]="article.featuredImage"
[alt]="article.title"
class="featured-image">
</div>
<div class="article-content">
<div class="content-main">
<markdown [data]="article.content"></markdown>
<div class="article-footer">
<div class="tags" *ngIf="article.tags && article.tags.length > 0">
<span class="tag" *ngFor="let tag of article.tags">
<i class="fas fa-tag"></i> {{tag}}
</span>
</div>
<div class="interactions">
<app-rating-widget
*ngIf="isAuthenticated"
[articleId]="article.id"
[currentRating]="article.avgRating"
(rated)="onRated($event)">
</app-rating-widget>
<app-bookmark-button
*ngIf="isAuthenticated"
[articleId]="article.id"
(bookmarked)="onBookmarked()">
</app-bookmark-button>
<app-share-buttons
[url]="'https://interimplaza.nl/kennisbank/' + article.slug"
[title]="article.title">
</app-share-buttons>
</div>
</div>
</div>
<div class="content-sidebar">
<app-related-articles
*ngIf="article.relatedArticles && article.relatedArticles.length > 0"
[articles]="article.relatedArticles">
</app-related-articles>
</div>
</div>
</div>
<div class="loading-spinner" *ngIf="loading">
<i class="fas fa-spinner fa-spin"></i> Laden...
</div>
<div class="error-message" *ngIf="error">
<i class="fas fa-exclamation-circle"></i> {{error}}
<a routerLink="/kennisbank" class="btn btn-primary">Terug naar Kennisbank</a>
</div>
File: frontend/recruitment-portal/src/app/components/kb/widgets/rating-widget/rating-widget.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-rating-widget',
standalone: true,
imports: [CommonModule],
template: `
<div class="rating-widget">
<div class="stars">
<i *ngFor="let star of [1,2,3,4,5]"
class="fas fa-star"
[class.filled]="star <= selectedRating"
[class.hover]="star <= hoverRating"
(mouseenter)="hoverRating = star"
(mouseleave)="hoverRating = 0"
(click)="rate(star)">
</i>
</div>
<span class="rating-text" *ngIf="currentRating > 0">
{{currentRating | number:'1.1-1'}} / 5.0
</span>
</div>
`,
styles: [`
.rating-widget {
display: flex;
align-items: center;
gap: 0.5rem;
.stars {
display: flex;
gap: 0.25rem;
i {
font-size: 1.5rem;
color: #ddd;
cursor: pointer;
transition: color 0.2s ease;
&.filled, &.hover {
color: #f39c12;
}
}
}
.rating-text {
font-size: 0.875rem;
color: #7f8c8d;
}
}
`]
})
export class RatingWidgetComponent {
@Input() articleId!: number;
@Input() currentRating: number = 0;
@Output() rated = new EventEmitter<number>();
selectedRating: number = 0;
hoverRating: number = 0;
rate(rating: number): void {
this.selectedRating = rating;
this.rated.emit(rating);
}
}
File: frontend/recruitment-portal/src/app/components/admin/kb/kb-editor/kb-editor.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { MarkdownModule } from 'ngx-markdown';
import { KbService } from '../../../../services/kb.service';
import { Article } from '../../../../models/article.model';
import { ArticleCategory, CATEGORY_LABELS } from '../../../../models/article-category.enum';
@Component({
selector: 'app-kb-editor',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, MarkdownModule],
templateUrl: './kb-editor.component.html',
styleUrls: ['./kb-editor.component.scss']
})
export class KbEditorComponent implements OnInit {
private fb = inject(FormBuilder);
private route = inject(ActivatedRoute);
private router = inject(Router);
private kbService = inject(KbService);
articleForm: FormGroup;
categories = Object.values(ArticleCategory);
categoryLabels = CATEGORY_LABELS;
editMode: boolean = false;
articleId: number | null = null;
previewMode: boolean = false;
saving: boolean = false;
constructor() {
this.articleForm = this.fb.group({
title: ['', [Validators.required, Validators.maxLength(200)]],
summary: ['', [Validators.required, Validators.maxLength(500)]],
content: ['', Validators.required],
category: ['', Validators.required],
tags: [''],
metaDescription: ['', Validators.maxLength(160)],
featuredImage: [''],
published: [false]
});
}
ngOnInit(): void {
this.route.paramMap.subscribe(params => {
const id = params.get('id');
if (id) {
this.editMode = true;
this.articleId = parseInt(id, 10);
this.loadArticle(this.articleId);
}
});
}
loadArticle(id: number): void {
// Load article for editing
// Implementation depends on admin API
}
onSubmit(): void {
if (this.articleForm.invalid) return;
this.saving = true;
const formValue = this.articleForm.value;
// Convert tags string to array
const tags = formValue.tags
? formValue.tags.split(',').map((t: string) => t.trim()).filter((t: string) => t)
: [];
const articleData = {
...formValue,
tags
};
const operation = this.editMode && this.articleId
? this.kbService.updateArticle(this.articleId, articleData)
: this.kbService.createArticle(articleData);
operation.subscribe({
next: (article) => {
console.log('Article saved successfully');
this.saving = false;
this.router.navigate(['/admin/kennisbank']);
},
error: (err) => {
console.error('Failed to save article', err);
this.saving = false;
}
});
}
togglePreview(): void {
this.previewMode = !this.previewMode;
}
get contentValue(): string {
return this.articleForm.get('content')?.value || '';
}
}
File: frontend/recruitment-portal/src/app/app.routes.ts
Add these routes:
import { Routes } from '@angular/router';
import { KbHomeComponent } from './components/kb/kb-home/kb-home.component';
import { ArticleListComponent } from './components/kb/article-list/article-list.component';
import { ArticleDetailComponent } from './components/kb/article-detail/article-detail.component';
import { SearchResultsComponent } from './components/kb/search-results/search-results.component';
import { KbEditorComponent } from './components/admin/kb/kb-editor/kb-editor.component';
import { KbListComponent } from './components/admin/kb/kb-list/kb-list.component';
import { authGuard } from './guards/auth.guard';
import { adminGuard } from './guards/admin.guard';
export const routes: Routes = [
// ... existing routes
// Public KB routes
{
path: 'kennisbank',
component: KbHomeComponent
},
{
path: 'kennisbank/categorie/:category',
component: ArticleListComponent
},
{
path: 'kennisbank/zoeken',
component: SearchResultsComponent
},
{
path: 'kennisbank/:slug',
component: ArticleDetailComponent
},
// Admin KB routes
{
path: 'admin/kennisbank',
component: KbListComponent,
canActivate: [authGuard, adminGuard]
},
{
path: 'admin/kennisbank/nieuw',
component: KbEditorComponent,
canActivate: [authGuard, adminGuard]
},
{
path: 'admin/kennisbank/bewerken/:id',
component: KbEditorComponent,
canActivate: [authGuard, adminGuard]
}
];
Always import shared stylesheets:
@import '../../../styles/shared/buttons';
@import '../../../styles/shared/forms';
@import '../../../styles/shared/cards';
// Mobile (default)
.component {
padding: 1rem;
}
// Tablet and up
@media (min-width: 768px) {
.component {
padding: 2rem;
}
}
// Desktop
@media (min-width: 1024px) {
.component {
padding: 3rem;
}
}
$primary-color: #0d6efd;
$secondary-color: #6c757d;
$success-color: #28a745;
$danger-color: #dc3545;
$warning-color: #ffc107;
$info-color: #17a2b8;
$text-primary: #2c3e50;
$text-secondary: #7f8c8d;
$text-muted: #95a5a6;
$bg-light: #f8f9fa;
$bg-white: #ffffff;
$border-color: #dee2e6;
article.model.ts with all interfacesarticle-category.enum.ts with labels and iconskb.service.ts with all API methodsngx-markdown and marked packagesapp.config.tsapp.routes.tsLast Updated: 2025-10-15 Status: Ready for Implementation Estimated Time: 20 hours (Week 2 of Sprint 5)
Reacties