feat(components): refactor and add BlogCard and BlogPost components

This commit is contained in:
Julien Oculi 2024-07-02 17:10:07 +02:00
parent 7cafcb5acd
commit 10f36ff4d3
6 changed files with 325 additions and 96 deletions

164
components/BlogBlocks.css Normal file
View file

@ -0,0 +1,164 @@
.components__blog_block {
min-width: 10rem;
aspect-ratio: 3 / 4;
display: flex;
flex-direction: column;
box-shadow: 0 0 0.4rem 0.2rem var(--_translucent);
border: var(--_border-size) solid transparent;
background-repeat: no-repeat;
background-size: 80%;
background-position: center var(--_gap);
backdrop-filter: blur(var(--_blur));
background-color: var(--_background-color);
&:has(a:focus-visible),
&:hover {
border: var(--_border-size) solid var(--_accent-color);
}
& h3 {
margin: 0;
padding: var(--_gap) var(--_gap-half);
backdrop-filter: blur(var(--_blur));
background-color: var(--_translucent);
border-bottom: 1px solid currentColor;
}
& a {
outline: none;
}
}
.components__blog_block--card {
max-width: 20rem;
}
.components__blog_block--placeholder {
animation: var(--animation-blink);
}
.components__blog_block--fallback {
opacity: 0.5;
}
:is(.components__blog_block--placeholder, .components__blog_block--fallback) {
h3 {
flex-grow: 1;
border: none;
align-content: center;
text-align: center;
}
}
.components__blog_block__spacer {
height: 0;
}
.components__blog_block--card .components__blog_block__spacer {
height: 30%;
}
.components__blog_block__links {
height: fit-content;
display: flex;
gap: var(--_gap-half);
justify-content: start;
padding: var(--_gap-half);
backdrop-filter: blur(var(--_blur));
background-color: var(--_translucent);
border-bottom: 1px solid currentColor;
& > a::before {
content: '🔗';
}
}
.components__blog_block__tags {
height: fit-content;
display: flex;
gap: var(--_gap-half);
justify-content: start;
padding: var(--_gap-half);
backdrop-filter: blur(var(--_blur));
background-color: var(--_translucent);
border-bottom: 1px solid currentColor;
& > span::before {
content: '#';
}
}
.components__blog_block__status {
display: block;
padding: var(--_gap-half);
background-color: var(--_translucent);
backdrop-filter: blur(var(--_blur));
}
.components__blog_block--card .components__blog_block__status {
position: absolute;
top: var(--_gap-half);
left: var(--_gap-half);
font-size: larger;
}
.components__blog_block__publisher {
display: block;
padding: var(--_gap-half);
background-color: var(--_translucent);
backdrop-filter: blur(var(--_blur));
}
.components__blog_block--card .components__blog_block__publisher {
position: absolute;
top: var(--_gap-half);
right: var(--_gap-half);
}
.components__blog_block__description {
text-wrap: balance;
flex-grow: 1;
backdrop-filter: blur(var(--_blur));
padding: var(--_gap-half);
}
.components__blog_block--card .components__blog_block__description {
min-height: 25%;
}
.components__blog_block__body {
text-wrap: balance;
flex-grow: 1;
padding: var(--_gap-half);
}
.components__blog_block__footer {
height: fit-content;
display: flex;
gap: var(--_gap);
justify-content: space-between;
padding: var(--_gap-half);
background-color: var(--_background-color);
}
.components__blog_post__infos {
display: flex;
gap: var(--_gap-half);
margin-block: var(--_gap-half);
& > * {
border: none;
padding: var(--_gap-half);
background-color: var(--_translucent);
}
}
.components__blog_post__description {
margin-block: var(--_gap-half);
font-family: var(--_font-family-code);
}
.components__blog_block--post {
width: var(--_readable-screen);
margin-inline: auto;
}

157
components/BlogBlocks.tsx Normal file
View file

@ -0,0 +1,157 @@
import { Markdown } from ':components/Markdown.tsx'
import { NewsFrontMatter } from ':src/blog/types.ts'
export type BlogProps = {
title: string
description: string
body: string
author: string
publisher: string
lastUpdate: Date
name: string
url: string
hash: string
options: NewsFrontMatter['x-cohabit']
tags: NewsFrontMatter['tags']
}
export function BlogCard(
{ title, description, author, lastUpdate, name, options, tags, publisher }:
BlogProps,
) {
return (
<div
class='components__blog_block components__blog_block--card'
style={{ backgroundImage: `url(${options.thumbnail})` }}
>
<div class='components__blog_block__spacer'></div>
<h3>
<a href={`/blog/${name}`}>{title}</a>
</h3>
<NewsLinks links={options.links} />
<NewsTags tags={tags} />
<span class='components__blog_block__publisher'>
{`@${publisher}`}
</span>
<NewsStatus status={options.status} />
<div class='components__blog_block__description'>
{description}
</div>
<NewsFooter author={author} lastUpdate={lastUpdate} />
</div>
)
}
export function BlogPost(
{
title,
description,
author,
lastUpdate,
body,
url,
options,
tags,
publisher,
}: BlogProps,
) {
return (
<div class='components__blog_block--post'>
<h1>{title}</h1>
<div class='components__blog_post__infos'>
<span class='components__blog_block__publisher'>
{`@${publisher}`}
</span>
<NewsStatus status={options.status} long />
<NewsLinks links={options.links} />
<NewsTags tags={tags} />
</div>
<div class='components__blog_post__infos'>
<span>{`Visibilité : ${options.visibility}`}</span>
<span>
{`Date de délivrance : ${
new Date(options.dueDate).toLocaleString()
}`}
</span>
</div>
<div class='components__blog_post__description'>
{description}
</div>
<div class='components__blog_post__body'>
<Markdown options={{ allowMath: true, baseUrl: url }}>
{body}
</Markdown>
</div>
<NewsFooter author={author} lastUpdate={lastUpdate} />
</div>
)
}
function NewsTags({ tags }: Pick<BlogProps, 'tags'>) {
return (
<div class='components__blog_block__tags'>
{tags
? tags.map((tag) => <span>{tag}</span>)
: <span>Aucun tag</span>}
</div>
)
}
function NewsFooter(
{ author, lastUpdate }: Pick<BlogProps, 'author' | 'lastUpdate'>,
) {
return (
<div class='components__blog_block__footer'>
<div>
<i class='ri-quill-pen-line'></i>
<span>{author}</span>
</div>
<div>
<i class='ri-refresh-line'></i>
<span>{lastUpdate.toLocaleDateString()}</span>
</div>
</div>
)
}
function NewsLinks({ links }: Pick<BlogProps['options'], 'links'>) {
return (
<div class='components__blog_block__links'>
{links
? links.flatMap(Object.entries).map((
[name, link],
) => <a href={link} target='_blank' title={name}>{name}</a>)
: <span>Aucun lien rapide</span>}
</div>
)
}
function NewsStatus(
{ status, long = false }: Pick<BlogProps['options'], 'status'> & {
long?: boolean
},
) {
const title = status === 'canceled'
? 'Annulé'
: status === 'current'
? 'En cours'
: status === 'finished'
? 'Terminé'
: 'Prévu'
return (
<span
class='components__blog_block__status'
title={title}
>
{status === 'canceled'
? <i class='ri-calendar-close-line'></i>
: status === 'current'
? <i class='ri-calendar-2-line'></i>
: status === 'finished'
? <i class='ri-calendar-check-line'></i>
: <i class='ri-calendar-2-line'></i>}
{long ? ` ${title}` : ''}
</span>
)
}

View file

@ -1,41 +0,0 @@
.components__blog_card {
min-width: 10rem;
aspect-ratio: 3 / 4;
display: flex;
flex-direction: column;
padding: var(--_gap-half);
gap: var(--_gap);
box-shadow: 0 0 0.4rem 0.2rem var(--_translucent);
border: var(--_border-size) solid transparent;
background-repeat: no-repeat;
background-size: contain;
&:has(a:focus-visible),
&:hover {
border: var(--_border-size) solid var(--_accent-color);
}
& h3 {
margin: 0;
}
& a {
outline: none;
}
}
.components__blog_card__spacer {
height: 50%;
}
.components__blog_card__text {
text-wrap: balance;
flex-grow: 1;
}
.components__blog_card__footer {
height: fit-content;
display: flex;
gap: var(--_gap);
justify-content: space-between;
}

View file

@ -1,54 +0,0 @@
type BlogCardProps = {
img: string
title: string
text: string
author: string
lasUpdate: Date
id: string
}
export function BlogCard(
{ img, title, text, author, lasUpdate, id }: BlogCardProps,
) {
return (
<div class='components__blog_card' style={{ backgroundImage: img }}>
<div class='components__blog_card__spacer'></div>
<h3>
<a href={`/blog/${id}`}>{title}</a>
</h3>
<div class='components__blog_card__text'>
{`${text.slice(0, 150)} ...`}
</div>
<div class='components__blog_card__footer'>
<div>
<i class='ri-quill-pen-line'></i>
<span>{author}</span>
</div>
<div>
<i class='ri-refresh-line'></i>
<span>{lasUpdate.toLocaleDateString()}</span>
</div>
</div>
</div>
)
}
const text =
'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Qui, perferendis enim blanditiis consequatur at porro quod, eligendi alias recusandae modi aliquam non? Quos voluptates quisquam provident animi nisi in ratione.'
export const blogMock: BlogCardProps[] = Array(50).fill(undefined).map(
(_, index) => {
return {
author: 'PGP',
lasUpdate: randomDate(),
title: `Some title here ${index}`,
text,
img: `url("https://picsum.photos/id/${index}/300/200")`,
id: String(index),
}
},
)
function randomDate() {
return new Date(Date.now() - Math.random() * 1e10)
}

View file

@ -44,7 +44,10 @@
"@simplewebauthn/server": "npm:@simplewebauthn/server@^10.0.0",
"@simplewebauthn/types": "npm:@simplewebauthn/types@^10.0.0",
"@std/encoding": "jsr:@std/encoding@^0.224.3",
"@std/front-matter": "jsr:@std/front-matter@^0.224.2",
"@std/http": "jsr:@std/http@^0.224.4",
"@std/json": "jsr:@std/json@^0.224.1",
"@std/streams": "jsr:@std/streams@^0.224.5",
"@univoq/": "https://deno.land/x/univoq@0.2.0/",
"gfm": "https://deno.land/x/gfm@0.6.0/mod.ts",
"preact": "https://esm.sh/preact@10.19.6",

View file

@ -3,7 +3,7 @@
@import url('../../components/Heros.css');
@import url('../../components/SponsorCards.css');
@import url('../../components/CohabitInfoTable.css');
@import url('../../components/BlogCard.css');
@import url('../../components/BlogBlocks.css');
@import url('../../components/MachineCard.css');
@import url('../../components/ProjectCard.css');
@import url('../../components/AutoGrid.css');