Skip to content

Commit

Permalink
feat: add example of modelview component
Browse files Browse the repository at this point in the history
  • Loading branch information
jterrazz committed Dec 23, 2024
1 parent 3548bcb commit 6738c81
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 50 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:20-alpine
FROM node:22-alpine

WORKDIR /home

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"author": "Jean-Baptiste Terrazzoni <jterrazzoni@gmail.com>",
"type": "module",
"engines": {
"node": "20.x.x"
"node": "22.x.x"
},
"scripts": {
"build": "next build",
Expand Down
2 changes: 1 addition & 1 deletion src/app/articles/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export async function generateMetadata({ params }: ArticlePageProps): Promise<Me
const article = await articlesRepository.getArticleByIndex(id);

return {
title: article?.metadata.title + ' ~ Jterrazz',
description: article?.metadata.description,
title: article?.metadata.title + ' ~ Jterrazz',
};
}

Expand Down
17 changes: 11 additions & 6 deletions src/app/articles/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Metadata } from 'next';

import { ArticleInMemoryRepository } from '../../infrastructure/repositories/article-in-memory.repository.js';
import { UserInMemoryRepository } from '../../infrastructure/repositories/user-in-memory.repository.js';

import { ArticlesListTemplate } from '../../components/templates/articles-list.template.js';
import { ArticlesListViewModelImpl } from '../../components/templates/articles-list.template.view-model.js';

export const metadata: Metadata = {
description: 'A collection of articles on coding, product concepts, and more.',
Expand All @@ -11,17 +13,20 @@ export const metadata: Metadata = {

export default async function ArticlesPage() {
const articlesRepository = new ArticleInMemoryRepository();
const userRepository = new UserInMemoryRepository();
const articles = await articlesRepository.getArticles();

// TODO: Move to template directly
const highlightTitle = 'Articles';
const highlightDescription =
'Dive into my articles. From coding and new product concepts, explore new things.';

return (
<ArticlesListTemplate
articles={articles}
highlightTitle={highlightTitle}
highlightDescription={highlightDescription}
/>
const viewModel = new ArticlesListViewModelImpl(
articles,
highlightTitle,
highlightDescription,
userRepository,
);

return <ArticlesListTemplate viewModel={viewModel.getViewModel()} />;
}
10 changes: 6 additions & 4 deletions src/components/templates/article.template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ type ArticleTemplateProps = {
articles: Article[];
};
import Script from 'next/script';

import { Article } from '../../domain/article.js';
import { ArticleRow } from './articles-list.template.jsx';
import { HeadingSection } from '../atoms/typography/heading-section.jsx';

import { HighlightedText } from '../atoms/highlighted-text.jsx';
import { HeadingSection } from '../atoms/typography/heading-section.jsx';


// TODO Move to viewmodel
export const ArticleTemplate: React.FC<ArticleTemplateProps> = ({
Expand Down Expand Up @@ -51,15 +53,15 @@ export const ArticleTemplate: React.FC<ArticleTemplateProps> = ({
<HeadingSection className="mt-6 md:mt-12 flex flex-col items-center">
<HighlightedText className="pr-2">Featured Posts</HighlightedText>
</HeadingSection>
{filteredArticles
{/* {filteredArticles
.sort(
(a, b) =>
new Date(b.metadata.datePublished).getTime() -
new Date(a.metadata.datePublished).getTime(),
)
.map((article) => (
<ArticleRow key={article.index} article={article} />
))}
))} */}
</MainContainer>
);
};
59 changes: 23 additions & 36 deletions src/components/templates/articles-list.template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,50 @@
import React from 'react';
import Link from 'next/link';

import { Article } from '../../domain/article.js';

import { HighlightedText } from '../atoms/highlighted-text.jsx';
import { HeadingSection } from '../atoms/typography/heading-section.js';
import { Highlight } from '../molecules/typography/highlight.js';
import { MainContainer } from '../organisms/main-container.jsx';
import { Badge, BadgeColor } from '../atoms/status/badge.jsx';
import { UserContactType } from '../../domain/user.js';
import { UserInMemoryRepository } from '../../infrastructure/repositories/user-in-memory.repository.js';
import { HeadingSection } from '../atoms/typography/heading-section.jsx';
import { Highlight } from '../molecules/typography/highlight.jsx';
import { MainContainer } from '../organisms/main-container.jsx';

export const ArticleRow: React.FC<{ article: Article }> = ({ article }) => {
const color = article.metadata.category === 'code' ? BadgeColor.Green : BadgeColor.Blue;
import {
ArticleRowViewModel,
ArticlesListViewModel,
} from './articles-list.template.view-model.jsx';

export const ArticleRow: React.FC<{ article: ArticleRowViewModel }> = ({ article }) => {
const color = article.isCodeCategory ? BadgeColor.Green : BadgeColor.Blue;

return (
<Link href={`/articles/${article.index}`}>
<div className="flex items-center justify-between border-b border-black-and-white py-3">
<h3 className="font-medium">{article.metadata.title}</h3>
<Badge value={article.metadata.category.toUpperCase()} color={color} />
<h3 className="font-medium">{article.title}</h3>
<Badge value={article.category} color={color} />
</div>
</Link>
);
};

type ArticlesListTemplateProps = {
highlightTitle: string;
highlightDescription: string;
articles: Article[];
viewModel: ArticlesListViewModel;
};

// TODO Move to viewmodel
export const ArticlesListTemplate: React.FC<ArticlesListTemplateProps> = ({
articles,
highlightTitle,
highlightDescription,
}) => {
const filteredArticles = articles.filter((article) => article.published);
const button = {
text: 'Follow me on Medium',
href: new UserInMemoryRepository().getContact(UserContactType.Medium).url.toString(),
};

export const ArticlesListTemplate: React.FC<ArticlesListTemplateProps> = ({ viewModel }) => {
return (
<MainContainer>
<Highlight title={highlightTitle} description={highlightDescription} button={button} />
<Highlight
title={viewModel.highlightTitle}
description={viewModel.highlightDescription}
button={viewModel.button}
/>

<HeadingSection>
<HighlightedText className="pr-2">Posts</HighlightedText>
</HeadingSection>
{filteredArticles
.sort(
(a, b) =>
new Date(b.metadata.datePublished).getTime() -
new Date(a.metadata.datePublished).getTime(),
)
.map((article) => (
<ArticleRow key={article.index} article={article} />
))}

{viewModel.articles.map((article) => (
<ArticleRow key={article.index} article={article} />
))}
</MainContainer>
);
};
62 changes: 62 additions & 0 deletions src/components/templates/articles-list.template.view-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Article } from '../../domain/article.js';
import { UserContactType, UserRepository } from '../../domain/user.js';

export interface ArticlesListButton {
href: string;
text: string;
}

export interface ArticleRowViewModel {
index: string;
title: string;
category: string;
isCodeCategory: boolean;
}

export interface ArticlesListViewModel {
highlightTitle: string;
highlightDescription: string;
button: ArticlesListButton;
articles: ArticleRowViewModel[];
}

export interface ViewModel<T> {
getViewModel: () => T;
}

export class ArticlesListViewModelImpl implements ViewModel<ArticlesListViewModel> {
constructor(
private readonly articles: Article[],
private readonly highlightTitle: string,
private readonly highlightDescription: string,
private readonly userRepo: UserRepository,
) {}

getViewModel(): ArticlesListViewModel {
const button = {
href: this.userRepo.getContact(UserContactType.Medium).url.toString(),
text: 'Follow me on Medium',
};

const sortedArticles = this.articles
.filter((article) => article.published)
.sort(
(a, b) =>
new Date(b.metadata.datePublished).getTime() -
new Date(a.metadata.datePublished).getTime(),
)
.map((article) => ({
category: article.metadata.category,
index: String(article.index),
isCodeCategory: article.metadata.category === 'code',
title: article.metadata.title,
}));

return {
articles: sortedArticles,
button,
highlightDescription: this.highlightDescription,
highlightTitle: this.highlightTitle,
};
}
}

0 comments on commit 6738c81

Please sign in to comment.