Skip to content

Commit

Permalink
feat: render the content of an article
Browse files Browse the repository at this point in the history
  • Loading branch information
valerymelou committed Jul 18, 2024
1 parent 23fddea commit 067cdba
Show file tree
Hide file tree
Showing 12 changed files with 458 additions and 356 deletions.
31 changes: 31 additions & 0 deletions apps/website/src/assets/images/looper-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions apps/website/src/assets/images/looper.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 8 additions & 1 deletion apps/website/src/styles.scss

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion libs/blog/data-access/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from './lib/article';
export * from './lib/article.service';
export * from './lib/asset';
export * from './lib/results';
export * from './lib/tag';
export * from './lib/tag.service';
export * from './lib/results';
154 changes: 150 additions & 4 deletions libs/blog/feature-article/src/lib/blog-article.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
@if (article$ | async; as article) {
<article class="relative my-10 max-w-3xl pt-10">
<article class="relative max-w-3xl pt-10">
<a
routerLink="/blog"
ui-flat-button
color="accent"
aria-label="Go back"
class="mb-4"
>
<ng-icon name="bootstrapArrowLeft" strokeWidth="5" size="20"></ng-icon>
</a>
<dl>
<dt class="sr-only">Date</dt>
<dd class="top-0 whitespace-nowrap text-sm leading-6">
Expand All @@ -14,10 +23,147 @@
{{ article.title }}
</h1>

<h2>
@if (article.cover) {
<figure>
<img
class="h-96 w-full rounded-lg object-cover object-center"
[src]="article.cover.url"
[alt]="article.cover.title"
/>
<figcaption class="italic text-slate-500 dark:text-slate-400">
{{ article.cover.title }}
</figcaption>
</figure>
}

<p class="my-10">
{{ article.abstract }}
</h2>
</p>

<hr class="my-10 border-slate-200 dark:border-slate-700" />

<div class="mb-10">
<div [cfRichTextDocument]="article.content">
<ng-container *cfRichTextNode="BLOCKS.HEADING_1; let node = node">
<h1
class="mb-4 text-2xl font-bold leading-[3rem] tracking-tight text-black lg:text-4xl dark:text-white"
>
<ng-container [cfRichTextChildren]="node" />
</h1>
</ng-container>
<ng-container *cfRichTextNode="BLOCKS.HEADING_2; let node = node">
<h2
class="mb-4 text-xl font-bold leading-[3rem] tracking-tight text-black lg:text-2xl dark:text-white"
>
<ng-container [cfRichTextChildren]="node" />
</h2>
</ng-container>
<ng-container *cfRichTextNode="BLOCKS.HEADING_3; let node = node">
<h3
class="mb-4 text-lg font-bold leading-[3rem] tracking-tight text-black lg:text-xl dark:text-white"
>
<ng-container [cfRichTextChildren]="node" />
</h3>
</ng-container>
<ng-container *cfRichTextNode="BLOCKS.HEADING_4; let node = node">
<h4
class="mb-4 text-base font-bold leading-[3rem] tracking-tight text-black lg:text-lg dark:text-white"
>
<ng-container [cfRichTextChildren]="node" />
</h4>
</ng-container>
<ng-container *cfRichTextNode="BLOCKS.HEADING_5; let node = node">
<h5
class="mb-4 font-bold leading-[3rem] tracking-tight text-black dark:text-white"
>
<ng-container [cfRichTextChildren]="node" />
</h5>
</ng-container>
<ng-container *cfRichTextNode="BLOCKS.HEADING_6; let node = node">
<h6
class="mb-4 font-bold leading-[3rem] tracking-tight text-black dark:text-white"
>
<ng-container [cfRichTextChildren]="node" />
</h6>
</ng-container>
<ng-container *cfRichTextNode="BLOCKS.PARAGRAPH; let node = node">
<p class="mb-4">
<ng-container [cfRichTextChildren]="node" />
</p>
</ng-container>
<ng-container *cfRichTextNode="BLOCKS.UL_LIST; let node = node">
<ul class="mb-4 list-disc pl-4">
<ng-container [cfRichTextChildren]="node" />
</ul>
</ng-container>
<ng-container *cfRichTextNode="BLOCKS.OL_LIST; let node = node">
<ol class="mb-4 list-decimal pl-4">
<ng-container [cfRichTextChildren]="node" />
</ol>
</ng-container>
<ng-container *cfRichTextNode="BLOCKS.LIST_ITEM; let node = node">
<li class="mb-4 pl-2">
<ng-container [cfRichTextChildren]="node" />
</li>
</ng-container>
<ng-container *cfRichTextNode="BLOCKS.QUOTE; let node = node">
<blockquote class="mb-4 border-l-4 border-gray-500 pl-4 italic">
<ng-container [cfRichTextChildren]="node" />
</blockquote>
</ng-container>
<ng-container *cfRichTextNode="BLOCKS.HR">
<hr class="my-10 border-slate-200 dark:border-slate-700" />
</ng-container>
<ng-container *cfRichTextNode="INLINES.HYPERLINK; let node = node">
<a
ui-link
[href]="node.data.uri"
target="_blank"
rel="noopener noreferrer"
>
<ng-container [cfRichTextChildren]="node" />
</a>
</ng-container>

<ng-container *cfRichTextNode="BLOCKS.EMBEDDED_ASSET; let node = node">
<figure class="mb-4">
<img
class="h-auto w-full"
[src]="node.data.target.fields.file.url"
[alt]="node.data.target.fields.title"
/>
@if (node.data.target.fields.title) {
<figcaption class="italic text-slate-500 dark:text-slate-400">
{{ node.data.target.fields.title }}
</figcaption>
}
</figure>
</ng-container>

<ng-container *cfRichTextNode="BLOCKS.EMBEDDED_ENTRY; let node = node">
Embedded entry
</ng-container>

<strong
*cfRichTextMark="MARKS.BOLD; let node = node"
class="dark:text-white"
>{{ node.value }}</strong
>

<div class="my-10"></div>
<ng-container *cfRichTextMark="MARKS.CODE; let node = node">
@if (node.value.split('\n').length > 1) {
<pre
class="bg-dark dark:bg-secondary-dark overflow-auto rounded-md p-4 text-white"
><code [innerHTML]="highlightCode(node.value)"></code></pre>
} @else {
<div
class="bg-dark dark:bg-secondary-dark inline-block rounded-md px-2 leading-5 text-white"
>
<code [innerHTML]="highlightCode(node.value)"></code>
</div>
}
</ng-container>
</div>
</div>
</article>
}
14 changes: 11 additions & 3 deletions libs/blog/feature-article/src/lib/blog-article.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { ActivatedRoute, provideRouter } from '@angular/router';

import { BlogArticleComponent } from './blog-article.component';
import { ArticleService, Article } from '@valerymelou/blog/data-access';
import { of } from 'rxjs';

import { ArticleService, Article } from '@valerymelou/blog/data-access';

import { BlogArticleComponent } from './blog-article.component';

describe('BlogArticleComponent', () => {
let component: BlogArticleComponent;
let fixture: ComponentFixture<BlogArticleComponent>;
Expand All @@ -14,6 +16,12 @@ describe('BlogArticleComponent', () => {
imports: [BlogArticleComponent],
providers: [
provideRouter([]),
{
provide: ActivatedRoute,
useValue: {
params: of({ slug: 'test' }),
},
},
{
provide: ArticleService,
useValue: {
Expand Down
98 changes: 92 additions & 6 deletions libs/blog/feature-article/src/lib/blog-article.component.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,70 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Params } from '@angular/router';
import {
AfterViewChecked,
Component,
Inject,
OnDestroy,
OnInit,
Renderer2,
} from '@angular/core';
import { CommonModule, DOCUMENT } from '@angular/common';
import { ActivatedRoute, Params, RouterModule } from '@angular/router';

import { map, Observable } from 'rxjs';
import { BLOCKS, INLINES, MARKS } from '@contentful/rich-text-types';

import { highlight, languages, highlightAll } from 'prismjs';
import 'prismjs/components/prism-python';
import 'prismjs/components/prism-rust';

import {
CfRichTextChildrenDirective,
CfRichTextMarkDirective,
CfRichTextNodeDirective,
CfRichTextDocumentComponent,
} from '@flowup/contentful-rich-text-angular-renderer';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { bootstrapArrowLeft } from '@ng-icons/bootstrap-icons';

import { Article, ArticleService } from '@valerymelou/blog/data-access';
import { MetadataService } from '@valerymelou/shared/seo';
import { ButtonComponent, LinkComponent } from '@valerymelou/shared/ui';

@Component({
selector: 'blog-article',
standalone: true,
imports: [CommonModule],
imports: [
CommonModule,
RouterModule,
CfRichTextDocumentComponent,
CfRichTextNodeDirective,
CfRichTextMarkDirective,
CfRichTextChildrenDirective,
NgIconComponent,
ButtonComponent,
LinkComponent,
],
templateUrl: './blog-article.component.html',
viewProviders: [provideIcons({ bootstrapArrowLeft })],
})
export class BlogArticleComponent {
export class BlogArticleComponent
implements AfterViewChecked, OnInit, OnDestroy
{
article$!: Observable<Article>;
loaded = false;
ready = false;

readonly BLOCKS = BLOCKS;
readonly MARKS = MARKS;
readonly INLINES = INLINES;

private codeHighlightCssLink!: HTMLLinkElement;

constructor(
private renderer: Renderer2,
private route: ActivatedRoute,
private articleService: ArticleService,
private metadataService: MetadataService,
@Inject(DOCUMENT) private document: Document,
) {
this.route.params.subscribe({
next: (params: Params) => {
Expand All @@ -28,17 +73,58 @@ export class BlogArticleComponent {
});
}

ngAfterViewChecked(): void {
if (this.loaded && !this.ready) {
highlightAll();
}
}

ngOnInit(): void {
this.loadCodeHighlightLib();
}

getArticle(slug: string): void {
slug = slug.split('-').slice(3).join('-');
this.article$ = this.articleService.getOne(slug).pipe(
map((article: Article) => {
this.metadataService.updateMetadata({
title: article.title,
description: article.abstract,
image: article.cover?.url,
image: article.cover?.url ?? '',
});
this.loaded = true;
return article;
}),
);
}

highlightCode(code: string): string {
let language = code.split('\n')[0].replace('```', '');

if (languages[language]) {
code = code.replace('```' + language + '\n', '');
} else {
language = 'javascript';
}

return highlight(code, languages[language], language);
}

private loadCodeHighlightLib(): void {
this.codeHighlightCssLink = this.renderer.createElement('link');
this.codeHighlightCssLink.rel = 'stylesheet';
this.codeHighlightCssLink.href =
'https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/themes/prism-okaidia.min.css';
this.codeHighlightCssLink.integrity =
'sha512-5HvW0a7ihK3ro2KhwEksDHXgIezsTeZybZDIn8d8Y015Ny+t7QWSIjnlCTjFzlK7Klb604HLGjsNqU/i5mJLjQ==';
this.codeHighlightCssLink.crossOrigin = 'anonymous';
this.codeHighlightCssLink.referrerPolicy = 'no-referrer';
this.renderer.appendChild(this.document.head, this.codeHighlightCssLink);
}

ngOnDestroy(): void {
if (this.codeHighlightCssLink) {
this.renderer.removeChild(this.document.head, this.codeHighlightCssLink);
}
}
}
2 changes: 1 addition & 1 deletion libs/blog/feature-home/project.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "feature-home",
"name": "blog-home",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/blog/feature-home/src",
"prefix": "blog",
Expand Down
6 changes: 6 additions & 0 deletions libs/shared/layout/src/lib/footer/footer.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
>GitHub</a
>.
</p>
<p class="lg:text-right">
All the content is from me and saved on
<a href="https://contentful.com" ui-link target="_blank" rel="noopener"
>Contentful</a
>.
</p>
</div>
</div>
</footer>
4 changes: 3 additions & 1 deletion libs/shared/ui/src/lib/link/link.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ export class LinkComponent {
this.getHostElement().classList.add(
'text-accent-base',
'dark:text-white',
'dark:hover:text-accent-base',
'font-medium',
'transition-all',
'duration-300',
'ease-in-out',
'dark:border-b',
'dark:border-blue-500',
'dark:hover:border-b-2',
);
}

Expand Down
Loading

0 comments on commit 067cdba

Please sign in to comment.