feat(infra): migrate to rsbuild
This commit is contained in:
95 files changed
+5582
-11282
No files matched your search
@@ -1,7 +1,5 @@
|
||||
import path from 'path';
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
import { mergeConfig } from 'vite';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import type { StorybookConfig } from 'storybook-react-rsbuild';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
@@ -13,28 +11,18 @@ const config: StorybookConfig = {
|
||||
'@storybook/addon-coverage',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
name: 'storybook-react-rsbuild',
|
||||
options: {},
|
||||
},
|
||||
typescript: {
|
||||
check: false,
|
||||
reactDocgen: 'react-docgen-typescript',
|
||||
reactDocgenTypescriptOptions: {
|
||||
shouldExtractLiteralValuesFromEnum: true,
|
||||
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
|
||||
},
|
||||
},
|
||||
async viteFinal(config) {
|
||||
return mergeConfig(config, {
|
||||
plugins: [tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'#': path.resolve(__dirname, '../src'),
|
||||
'@ermnvldmr/stl': path.resolve(__dirname, '../../../packages/stl/src'),
|
||||
'@ermnvldmr/ui': path.resolve(__dirname, '../../../packages/ui/src'),
|
||||
},
|
||||
},
|
||||
});
|
||||
rsbuildFinal: (config) => {
|
||||
config.resolve ||= {};
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
'#': path.resolve(__dirname, '../src'),
|
||||
'@ermnvldmr/stl': path.resolve(__dirname, '../../../packages/stl/src'),
|
||||
'@ermnvldmr/ui': path.resolve(__dirname, '../../../packages/ui/src'),
|
||||
};
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// @ts-check
|
||||
import mdx from '@astrojs/mdx';
|
||||
import react from '@astrojs/react';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://www.ermnvldmr.com',
|
||||
output: 'static',
|
||||
srcDir: './src',
|
||||
integrations: [mdx(), sitemap(), react()],
|
||||
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@ermnvldmr/stl': new URL('../../packages/stl/src', import.meta.url).pathname,
|
||||
'@ermnvldmr/ui': new URL('../../packages/ui/src', import.meta.url).pathname,
|
||||
'#': new URL('./src/', import.meta.url).pathname,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import * as TestArticle from './articles/2026/test-article.mdx';
|
||||
|
||||
export interface Article {
|
||||
slug: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
createdDate: Date;
|
||||
updatedDate?: Date;
|
||||
tags?: string[];
|
||||
image?: string;
|
||||
Component: React.ComponentType<{ components?: Record<string, React.ComponentType<any>> }>;
|
||||
}
|
||||
|
||||
function register(module: any, slug: string): Article {
|
||||
const { frontmatter, default: Component } = module;
|
||||
return {
|
||||
slug,
|
||||
title: frontmatter.title,
|
||||
description: frontmatter.description,
|
||||
createdDate: new Date(frontmatter.createdDate),
|
||||
updatedDate: frontmatter.updatedDate ? new Date(frontmatter.updatedDate) : undefined,
|
||||
tags: frontmatter.tags,
|
||||
image: frontmatter.image,
|
||||
Component,
|
||||
};
|
||||
}
|
||||
|
||||
export const articles: Record<string, Article> = {
|
||||
'test-article': register(TestArticle, '2026/test-article'),
|
||||
};
|
||||
|
||||
export const sortedArticles = Object.values(articles).sort(
|
||||
(a, b) => b.createdDate.getTime() - a.createdDate.getTime()
|
||||
);
|
||||
@@ -1,18 +0,0 @@
|
||||
import { reactJestConfig } from '@ermnvldmr/jest-config/react';
|
||||
|
||||
export default {
|
||||
...reactJestConfig,
|
||||
moduleNameMapper: {
|
||||
...reactJestConfig.moduleNameMapper,
|
||||
'^#(.*)$': '<rootDir>/src/$1',
|
||||
'^@ermnvldmr/ui/(.*)$': '<rootDir>/../../packages/ui/src/$1',
|
||||
'^@ermnvldmr/ui$': '<rootDir>/../../packages/ui/src',
|
||||
},
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{ts,tsx}',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/**/*.stories.{ts,tsx}',
|
||||
'!src/**/*.test.{ts,tsx}',
|
||||
'!src/**/*.astro',
|
||||
],
|
||||
};
|
||||
+17
-20
@@ -4,16 +4,15 @@
|
||||
"description": "www.ermnvldmr.com website",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"dev": "rsbuild dev",
|
||||
"build": "rsbuild build",
|
||||
"preview": "rsbuild preview",
|
||||
"lint": "eslint src --max-warnings 0",
|
||||
"format": "prettier --write .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "jest --passWithNoTests",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage --passWithNoTests",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:storybook": "test-storybook --coverage",
|
||||
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"http-server storybook-static --port 6006 --silent\" \"wait-on tcp:127.0.0.1:6006 && pnpm run test:storybook\"",
|
||||
"check": "pnpm run lint && pnpm run typecheck && pnpm run test",
|
||||
@@ -23,13 +22,8 @@
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.2.5",
|
||||
"@astrojs/react": "^4.2.5",
|
||||
"@astrojs/rss": "^4.0.11",
|
||||
"@astrojs/sitemap": "^3.3.1",
|
||||
"@ermnvldmr/stl": "workspace:*",
|
||||
"@ermnvldmr/ui": "workspace:*",
|
||||
"astro": "^5.7.5",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"simplex-noise": "^4.0.3",
|
||||
@@ -38,8 +32,10 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ermnvldmr/eslint-config": "workspace:*",
|
||||
"@ermnvldmr/jest-config": "workspace:*",
|
||||
"@ermnvldmr/rsbuild-config": "workspace:*",
|
||||
"@ermnvldmr/storybook-config": "workspace:*",
|
||||
"@rsbuild/core": "^1.2.11",
|
||||
"@rsbuild/plugin-mdx": "^1.1.1",
|
||||
"@storybook/addon-a11y": "^8.6.15",
|
||||
"@storybook/addon-coverage": "^3.0.0",
|
||||
"@storybook/addon-essentials": "^8.6.15",
|
||||
@@ -49,28 +45,29 @@
|
||||
"@storybook/react-vite": "^8.6.15",
|
||||
"@storybook/test": "^8.6.15",
|
||||
"@storybook/test-runner": "^0.19.1",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.1",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@types/suncalc": "^1.9.2",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.25.1",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^30.2.0",
|
||||
"jest-environment-jsdom": "^30.2.0",
|
||||
"jsdom": "^28.0.0",
|
||||
"playwright": "^1.57.0",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-mdx-frontmatter": "^5.2.0",
|
||||
"storybook": "^8.6.15",
|
||||
"ts-jest": "^29.4.6",
|
||||
"storybook-react-rsbuild": "^0.1.10",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-istanbul": "^7.2.1",
|
||||
"vitest": "^4.0.18",
|
||||
"wait-on": "^8.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { defineServiceConfig } from '@ermnvldmr/rsbuild-config';
|
||||
import { pluginMdx } from '@rsbuild/plugin-mdx';
|
||||
import remarkFrontmatter from 'remark-frontmatter';
|
||||
import remarkMdxFrontmatter from 'remark-mdx-frontmatter';
|
||||
|
||||
export default defineServiceConfig({
|
||||
plugins: [
|
||||
pluginMdx({
|
||||
mdxLoaderOptions: {
|
||||
remarkPlugins: [remarkFrontmatter, [remarkMdxFrontmatter, { name: 'frontmatter' }]],
|
||||
},
|
||||
}),
|
||||
],
|
||||
source: {
|
||||
entry: {
|
||||
index: './src/app/index.tsx',
|
||||
'articles/index': './src/app/articles/index.tsx',
|
||||
'articles/2026/test-article': './src/app/articles/2026/test-article.tsx',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,12 +1,7 @@
|
||||
---
|
||||
import { Container, Header, Paragraph, Link, VStack } from '@ermnvldmr/ui';
|
||||
import React from 'react';
|
||||
|
||||
import IndexLayout from '#layouts/IndexLayout/IndexLayout.astro';
|
||||
import { SITE_TITLE, SITE_DESCRIPTION } from '#shared/constants';
|
||||
import '#global.css';
|
||||
---
|
||||
|
||||
<IndexLayout title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||
export const HomePage = (): React.JSX.Element => (
|
||||
<Container as="main" className="flex h-full w-full justify-center p-4">
|
||||
<VStack gap={4}>
|
||||
<Header level={2}>Welcome to my page!</Header>
|
||||
@@ -16,12 +11,11 @@ import '#global.css';
|
||||
It will be further configured.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
For the source code and support please refer to <Link href="https://github.com/deytenit/ermnvldmr.com/"
|
||||
>ermnvldmr.com monorepo</Link
|
||||
>.
|
||||
For the source code and support please refer to{' '}
|
||||
<Link href="https://github.com/deytenit/ermnvldmr.com/">ermnvldmr.com monorepo</Link>.
|
||||
<br />
|
||||
Pictures of mine are available at <Link href="https://ermnvldmr.com/instagram">@deytenit</Link
|
||||
>.
|
||||
Pictures of mine are available at{' '}
|
||||
<Link href="https://ermnvldmr.com/instagram">@deytenit</Link>.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Check out my <Link href="/articles">thoughts and articles on various matters</Link>.
|
||||
@@ -29,4 +23,4 @@ import '#global.css';
|
||||
<Paragraph italic>Thank you for visiting!</Paragraph>
|
||||
</VStack>
|
||||
</Container>
|
||||
</IndexLayout>
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { MARKDOWN_COMPONENTS } from '@ermnvldmr/ui';
|
||||
import React from 'react';
|
||||
|
||||
import { articles } from '../../../../content/registry';
|
||||
import { ArticleLayout } from '../../../layouts/ArticleLayout/ArticleLayout';
|
||||
|
||||
export const ArticlePage = (): React.JSX.Element => {
|
||||
const article = articles['test-article'];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!article) {
|
||||
throw new Error('Article not found');
|
||||
}
|
||||
|
||||
return (
|
||||
<ArticleLayout
|
||||
createdDate={article.createdDate}
|
||||
description={article.description}
|
||||
tags={article.tags}
|
||||
title={article.title}
|
||||
updatedDate={article.updatedDate}
|
||||
>
|
||||
<article.Component components={MARKDOWN_COMPONENTS} />
|
||||
</ArticleLayout>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ArticlePage } from './ArticlePage';
|
||||
import { articles } from '../../../../content/registry';
|
||||
import { createPage } from '../../../lib/createPage';
|
||||
|
||||
const article = articles['test-article'];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!article) {
|
||||
throw new Error('Article not found');
|
||||
}
|
||||
|
||||
createPage(ArticlePage, {
|
||||
title: `${article.title} - Vladimir Eremin`,
|
||||
description: article.description,
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Container, Header, Link, Separator, Article } from '@ermnvldmr/ui';
|
||||
import React from 'react';
|
||||
|
||||
import { sortedArticles } from '../../../content/registry';
|
||||
|
||||
export const ArticlesList = (): React.JSX.Element => (
|
||||
<Container as="main" className="mx-auto max-w-5xl px-6 py-20">
|
||||
<Header className="mb-12 text-center" level={1}>
|
||||
Articles
|
||||
</Header>
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||
{sortedArticles.map((article, index) => (
|
||||
<React.Fragment key={article.slug}>
|
||||
{index > 0 && index % 2 === 0 && (
|
||||
<div className="col-span-1 py-8 md:col-span-2">
|
||||
<Separator className="w-full" type="double" />
|
||||
</div>
|
||||
)}
|
||||
<Link className="group block h-full no-underline!" href={`/articles/${article.slug}`}>
|
||||
<Article
|
||||
additionalText={article.tags?.join(' • ')}
|
||||
headline={article.title}
|
||||
subHeadline={article.createdDate.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
>
|
||||
{article.description}
|
||||
</Article>
|
||||
</Link>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ArticlesList } from './ArticlesList';
|
||||
import { createPage } from '../../lib/createPage';
|
||||
import { SITE_TITLE } from '../../shared/constants';
|
||||
|
||||
createPage(ArticlesList, {
|
||||
title: `Articles | ${SITE_TITLE}`,
|
||||
description: 'Browse all articles and projects.',
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { HomePage } from './HomePage';
|
||||
import { createPage } from '../lib/createPage';
|
||||
import { SITE_DESCRIPTION, SITE_TITLE } from '../shared/constants';
|
||||
|
||||
createPage(HomePage, { title: SITE_TITLE, description: SITE_DESCRIPTION });
|
||||
@@ -1,18 +0,0 @@
|
||||
import { glob } from 'astro/loaders';
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
const articles = defineCollection({
|
||||
// Load all markdown and MDX files from the content/articles directory
|
||||
loader: glob({ pattern: '**/*.{md,mdx}', base: './content/articles' }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
// Astro handles date strings in frontmatter, but we use coerce to be safe
|
||||
createdDate: z.coerce.date(),
|
||||
updatedDate: z.coerce.date().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
image: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { articles };
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare module '*.mdx' {
|
||||
import type { ComponentType } from 'react';
|
||||
const Component: ComponentType<{ components?: Record<string, React.ComponentType> }>;
|
||||
export default Component;
|
||||
}
|
||||
+3
-19
@@ -1,19 +1,7 @@
|
||||
---
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { title, description } = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script is:inline>
|
||||
<script>
|
||||
const getThemePreference = () => {
|
||||
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
||||
return localStorage.getItem('theme');
|
||||
@@ -37,13 +25,9 @@ const { title, description } = Astro.props;
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="robots" content="index,follow" />
|
||||
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<link rel="canonical" href="https://www.ermnvldmr.com/" />
|
||||
<title>Vladimir Eremin</title>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Header, Paragraph, List, Blockquote, Code, CodeBlock, Container, Text } from '@ermnvldmr/ui';
|
||||
import {
|
||||
Header,
|
||||
Paragraph,
|
||||
List,
|
||||
Blockquote,
|
||||
Code,
|
||||
CodeBlock,
|
||||
Container,
|
||||
Text,
|
||||
} from '@ermnvldmr/ui';
|
||||
|
||||
import { ArticleLayout } from './ArticleLayout';
|
||||
|
||||
@@ -23,7 +32,7 @@ const meta: Meta<typeof ArticleLayout> = {
|
||||
export default meta;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
type Story = StoryObj<typeof ArticleLayout>;
|
||||
|
||||
@@ -94,7 +103,12 @@ export const collections = { articles };`}
|
||||
We can also include images and videos. The layout ensures they are responsive and fit the
|
||||
grid.
|
||||
</Paragraph>
|
||||
<Container border bg="muted" className="flex aspect-video items-center justify-center" rounded="lg">
|
||||
<Container
|
||||
border
|
||||
bg="muted"
|
||||
className="flex aspect-video items-center justify-center"
|
||||
rounded="lg"
|
||||
>
|
||||
<Text color="muted">Placeholder for Image/Video</Text>
|
||||
</Container>
|
||||
|
||||
@@ -102,4 +116,3 @@ export const collections = { articles };`}
|
||||
</ArticleLayout>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import '../global.css'; // Ensure global styles are available
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface PageOptions {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a React page component with hydration support.
|
||||
*
|
||||
* @param Component - The React component to render.
|
||||
* @param options - Page options like title.
|
||||
* @returns The page component.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* createPage(<App />);
|
||||
* ```
|
||||
*/
|
||||
export function createPage(Component: React.ComponentType, options: PageOptions = {}): void {
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
if (options.title) {
|
||||
document.title = options.title;
|
||||
}
|
||||
|
||||
if (rootElement) {
|
||||
createRoot(rootElement).render(
|
||||
<React.StrictMode>
|
||||
<Component />
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
import { MARKDOWN_COMPONENTS } from '@ermnvldmr/ui';
|
||||
import { getCollection, render } from 'astro:content';
|
||||
|
||||
import { ArticleLayout } from '#layouts/ArticleLayout/ArticleLayout';
|
||||
import IndexLayout from '#layouts/IndexLayout/IndexLayout.astro';
|
||||
import '#global.css';
|
||||
|
||||
/**
|
||||
* Generate paths for all articles in the collection.
|
||||
* The slug will include the year directory (e.g., "2026/test-article").
|
||||
*/
|
||||
export async function getStaticPaths() {
|
||||
const articles = await getCollection('articles');
|
||||
return articles.map((entry) => ({
|
||||
params: { slug: entry.id },
|
||||
props: { entry },
|
||||
}));
|
||||
}
|
||||
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await render(entry);
|
||||
const { title, description, createdDate, updatedDate, tags } = entry.data;
|
||||
---
|
||||
|
||||
<IndexLayout title={title} description={description ?? ''}>
|
||||
<ArticleLayout
|
||||
title={title}
|
||||
description={description}
|
||||
createdDate={createdDate}
|
||||
updatedDate={updatedDate}
|
||||
tags={tags}
|
||||
>
|
||||
<Content components={MARKDOWN_COMPONENTS} />
|
||||
</ArticleLayout>
|
||||
</IndexLayout>
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
import { Container, Header, Paragraph, Link, VStack, List, Time, Text } from '@ermnvldmr/ui';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
import IndexLayout from '#layouts/IndexLayout/IndexLayout.astro';
|
||||
import { SITE_TITLE } from '#shared/constants';
|
||||
import '#global.css';
|
||||
|
||||
const articles = (await getCollection('articles')).sort(
|
||||
(a, b) => b.data.createdDate.valueOf() - a.data.createdDate.valueOf()
|
||||
);
|
||||
---
|
||||
|
||||
<IndexLayout title={`Articles | ${SITE_TITLE}`} description="Browse all articles and projects.">
|
||||
<Container as="main" className="mx-auto max-w-3xl px-6 py-20">
|
||||
<Header level={1} className="mb-12">Articles</Header>
|
||||
<List variant="plain" spacing="l">
|
||||
{
|
||||
articles.map((article) => (
|
||||
<List.Item>
|
||||
<Link href={`/articles/${article.id}`} className="group block no-underline!">
|
||||
<VStack gap={3} as="article">
|
||||
<Time
|
||||
date={article.data.createdDate}
|
||||
color="muted"
|
||||
size="s"
|
||||
type="label"
|
||||
/>
|
||||
<Header
|
||||
level={2}
|
||||
className="group-hover:text-primary mt-0 border-none pb-0 text-3xl transition-colors"
|
||||
>
|
||||
{article.data.title}
|
||||
</Header>
|
||||
{article.data.description && (
|
||||
<Paragraph color="muted" size="l" maxLines={3}>
|
||||
{article.data.description}
|
||||
</Paragraph>
|
||||
)}
|
||||
{article.data.tags && (
|
||||
<div class="flex gap-2">
|
||||
{article.data.tags.map((tag) => (
|
||||
<Text color="muted" size="s" className="before:content-['#']">
|
||||
{tag}
|
||||
</Text>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</VStack>
|
||||
</Link>
|
||||
</List.Item>
|
||||
))
|
||||
}
|
||||
</List>
|
||||
</Container>
|
||||
</IndexLayout>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { reactVitestConfig } from '@ermnvldmr/rsbuild-config/vitest';
|
||||
import { mergeConfig } from 'vitest/config';
|
||||
|
||||
export default mergeConfig(reactVitestConfig, {
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
||||
setupFiles: ['./src/setupTests.ts'],
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user