1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2025-05-29 22:37:44 +02:00

Compare commits

...

25 Commits

Author SHA1 Message Date
a43bfb4a0d feat(portfolio): add Fusey 2025-05-11 17:28:02 +02:00
69af1bccc3 build: update to Node.js v24.0.0, pnpm v10, Tailwind CSS v4 2025-05-11 16:59:11 +02:00
cec70161f7 fix(blog): add useful command to Git cheatsheet post 2025-05-07 19:54:06 +02:00
f70a66e251 fix: replace standard open source contributions by nodejs.org 2025-02-08 20:56:56 +01:00
38eb296088 feat: themeColor metadata 2025-02-08 20:51:22 +01:00
43d91bfc28 chore: fix storybook configuration 2025-02-08 20:48:19 +01:00
b63cc3a66e chore: cleaner setup 2025-02-08 20:00:47 +01:00
270920111a chore(release): 4.1.3 [skip ci] 2025-01-23 12:17:18 +00:00
d91feb8de4 fix: use Plausible 2025-01-23 13:16:20 +01:00
e68cb08a6f fix: update Node.js to v22.13.1 (security release)
Ref: https://nodejs.org/en/blog/release/v22.13.1
2025-01-23 13:11:57 +01:00
09d677bd37 chore(release): 4.1.2 [skip ci] 2024-12-06 22:22:48 +00:00
db1159f20c fix(blog): issue with CSS loading
Ref: https://github.com/nodejs/node/issues/56155
2024-12-06 23:16:40 +01:00
af5c845e4b chore(release): 4.1.1 [skip ci] 2024-12-06 08:35:37 +00:00
3f66dfe46e fix: error 404 not found 2024-12-06 09:30:49 +01:00
d52a0c6f08 build(deps): update latest 2024-12-06 09:30:44 +01:00
251b0b4038 chore(release): 4.1.0 [skip ci] 2024-11-09 20:10:23 +00:00
a5baffe9eb fix(ui): button outline, hover effect 2024-11-09 21:01:31 +01:00
1351e4122d fix(blog): shiki syntax highlighting for txt 2024-11-09 20:21:09 +01:00
4c69d5a852 refactor(ui): allow all tailwind css colors 2024-11-09 20:14:30 +01:00
9e840b8dae build(deps): update Next.js to v15 and ESLint to v9 2024-11-09 19:50:22 +01:00
59153a7a69 style: fix ESLint 2024-10-13 00:00:44 +02:00
0a7094005c chore: config updates 2024-10-12 23:51:58 +02:00
12f1d6cdf2 chore: try to fix CI with Playwright 2024-09-12 00:14:58 +02:00
0d7b33727b feat: add in progress Engineering study + IRCAD until 2027 on CV 2024-09-12 00:05:59 +02:00
386f407f21 chore: simplify TypeScript config 2024-09-11 23:53:26 +02:00
194 changed files with 7482 additions and 10379 deletions

View File

@ -9,3 +9,6 @@ end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
[*.md]
indent_size = 4

View File

@ -1 +1,2 @@
TZ=Europe/Paris
WEBSITE_PORT=3000 WEBSITE_PORT=3000

View File

@ -6,8 +6,8 @@ labels: "bug"
--- ---
<!-- <!--
Please provide a clear and concise description of what the bug is. Include Please provide a clear and concise description of what the bug is. Include
screenshots if needed. Please make sure your issue has not already been fixed. screenshots if needed. Please make sure your issue has not already been fixed.
--> -->
## Steps To Reproduce ## Steps To Reproduce

View File

@ -10,17 +10,21 @@ jobs:
chromatic: chromatic:
timeout-minutes: 30 timeout-minutes: 30
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
env:
DO_NOT_TRACK: "1"
TURBO_TELEMETRY_DISABLED: "1"
NEXT_TELEMETRY_DISABLED: "1"
steps: steps:
- uses: "actions/checkout@v4.1.7" - uses: "actions/checkout@v4.2.2"
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: "pnpm/action-setup@v4.0.0" - uses: "pnpm/action-setup@v4.1.0"
- name: "Setup Node.js" - name: "Setup Node.js"
uses: "actions/setup-node@v4.0.3" uses: "actions/setup-node@v4.2.0"
with: with:
node-version: "22.x" node-version: "24.x"
cache: "pnpm" cache: "pnpm"
- name: "Install dependencies" - name: "Install dependencies"

View File

@ -10,15 +10,19 @@ jobs:
ci: ci:
timeout-minutes: 30 timeout-minutes: 30
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
env:
DO_NOT_TRACK: "1"
TURBO_TELEMETRY_DISABLED: "1"
NEXT_TELEMETRY_DISABLED: "1"
steps: steps:
- uses: "actions/checkout@v4.1.7" - uses: "actions/checkout@v4.2.2"
- uses: "pnpm/action-setup@v4.0.0" - uses: "pnpm/action-setup@v4.1.0"
- name: "Setup Node.js" - name: "Setup Node.js"
uses: "actions/setup-node@v4.0.3" uses: "actions/setup-node@v4.2.0"
with: with:
node-version: "22.x" node-version: "24.x"
cache: "pnpm" cache: "pnpm"
- name: "Install dependencies" - name: "Install dependencies"
@ -29,15 +33,15 @@ jobs:
- run: "node --run lint:editorconfig" - run: "node --run lint:editorconfig"
- run: "node --run lint:markdown" - run: "node --run lint:markdown"
- run: "node --run lint:prettier"
- run: "node --run lint:eslint"
- run: "node --run lint:typescript" - run: "node --run lint:typescript"
- run: "node --run lint:eslint"
- run: "node --run lint:prettier"
- run: "node --run test" - run: "node --run test"
- run: "node --run build" - run: "node --run build"
commitlint: commitlint:
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
steps: steps:
- uses: "actions/checkout@v4.1.7" - uses: "actions/checkout@v4.2.2"
- uses: "wagoid/commitlint-github-action@v6.0.1" - uses: "wagoid/commitlint-github-action@v6.1.2"

View File

@ -1,45 +0,0 @@
name: "Release"
on:
push:
branches: [main, staging]
jobs:
release:
timeout-minutes: 30
runs-on: "ubuntu-latest"
permissions:
contents: "write"
issues: "write"
pull-requests: "write"
id-token: "write"
steps:
- uses: "actions/checkout@v4.1.7"
with:
fetch-depth: 0
persist-credentials: false
- name: "Import GPG key"
uses: "crazy-max/ghaction-import-gpg@v6.0.0"
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
git_user_signingkey: true
git_commit_gpgsign: true
- uses: "pnpm/action-setup@v4.0.0"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.3"
with:
node-version: "22.x"
cache: "pnpm"
- name: "Install dependencies"
run: "pnpm install --frozen-lockfile"
- name: "Release"
run: "node --run release"
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_EMAIL }}

View File

@ -1,12 +0,0 @@
{
"config": {
"extends": "markdownlint/style/prettier",
"default": true,
"relative-links": true,
"no-duplicate-heading": false,
"no-inline-html": false,
},
"globs": ["**/*.md"],
"ignores": ["**/node_modules"],
"customRules": ["markdownlint-rule-relative-links"],
}

19
.markdownlint-cli2.mjs Normal file
View File

@ -0,0 +1,19 @@
import relativeLinksRule, { markdownIt } from "markdownlint-rule-relative-links"
const config = {
config: {
extends: "markdownlint/style/prettier",
default: true,
"relative-links": true,
"no-duplicate-heading": false,
"no-inline-html": false,
},
globs: ["**/*.md"],
ignores: ["**/node_modules"],
customRules: [relativeLinksRule],
markdownItFactory: () => {
return markdownIt
},
}
export default config

1
.npmrc
View File

@ -1 +0,0 @@
save-exact = true

View File

@ -1,34 +0,0 @@
{
"branches": ["main", { "name": "staging", "prerelease": true }],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/exec",
{
"prepareCmd": "replace-in-files --regex='version\": *\"[^\"]*' --replacement='\"version\": \"${nextRelease.version}\"' '**/package.json' '!**/node_modules/**'"
}
],
[
"@semantic-release/git",
{
"assets": [
"package.json",
"apps/*/package.json",
"packages/*/package.json"
],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}
],
"@semantic-release/github",
[
"@saithodev/semantic-release-backmerge",
{
"branches": [
{ "from": "main", "to": "develop" },
{ "from": "staging", "to": "develop" }
]
}
]
]
}

View File

@ -1,12 +1,12 @@
{ {
"recommendations": [ "recommendations": [
"Vercel.turbo-vsc",
"editorconfig.editorconfig", "editorconfig.editorconfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"davidanson.vscode-markdownlint",
"bradlc.vscode-tailwindcss", "bradlc.vscode-tailwindcss",
"mikestead.dotenv", "mikestead.dotenv",
"davidanson.vscode-markdownlint", "antfu.pnpm-catalog-lens",
"ms-azuretools.vscode-docker" "Lokalise.i18n-ally"
] ]
} }

View File

@ -22,7 +22,7 @@
"body": [ "body": [
"import type { Meta, StoryObj } from \"@storybook/react\"", "import type { Meta, StoryObj } from \"@storybook/react\"",
"", "",
"import { ${1:ComponentName} as ${1:ComponentName}Component } from \"./${1:ComponentName}\"", "import { ${1:ComponentName} as ${1:ComponentName}Component } from \"./${1:ComponentName}.tsx\"",
"", "",
"const meta = {", "const meta = {",
" title: \"${1:ComponentName}\",", " title: \"${1:ComponentName}\",",

15
.vscode/settings.json vendored
View File

@ -7,14 +7,17 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": "explicit", "source.fixAll": "explicit",
"source.organizeImports": "explicit" "source.organizeImports": "never"
}, },
"eslint.options": {
"ignorePath": ".gitignore"
},
"prettier.ignorePath": ".gitignore",
"tailwindCSS.experimental.classRegex": [ "tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"], ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"] ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
] ],
"i18n-ally.localesPaths": ["./packages/i18n/src/translations/"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": false,
"i18n-ally.sourceLanguage": "en-US",
"i18n-ally.displayLanguage": "en-US",
"i18n-ally.enabledFrameworks": ["next-intl", "general"],
"i18n-ally.extract.autoDetect": true
} }

View File

@ -20,21 +20,16 @@ community include:
- Demonstrating empathy and kindness toward other people - Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences - Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback - Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include: Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or - The use of sexualized language or imagery, and sexual attention or advances of any kind
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks - Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment - Public or private harassment
- Publishing others' private information, such as a physical or email - Publishing others' private information, such as a physical or email address, without their explicit permission
address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities ## Enforcement Responsibilities

View File

@ -31,8 +31,9 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
### Prerequisites ### Prerequisites
- [Node.js](https://nodejs.org/) >= 22.0.0 - [Node.js](https://nodejs.org/) >= v24.0.0 [(`nvm install 24`)](https://nvm.sh)
- [pnpm](https://pnpm.io/) >= 9.5.0 - [pnpm](https://pnpm.io/) v10.10.0 [(`npm install --global corepack@0.32.0 && corepack enable`)](https://github.com/nodejs/corepack)
- [Docker](https://www.docker.com/)
### Installation ### Installation
@ -40,6 +41,9 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
# Clone the repository # Clone the repository
git clone git@github.com:theoludwig/theoludwig.git git clone git@github.com:theoludwig/theoludwig.git
# Go to the project root
cd theoludwig
# Configure environment variables # Configure environment variables
cp .env.example .env cp .env.example .env
cp apps/website/.env.example apps/website/.env cp apps/website/.env.example apps/website/.env
@ -60,24 +64,28 @@ node --run dev
# Lint # Lint
node --run lint:editorconfig node --run lint:editorconfig
node --run lint:markdown node --run lint:markdown
node --run lint:prettier
node --run lint:eslint
node --run lint:typescript node --run lint:typescript
node --run lint:eslint
node --run lint:prettier
# Tests # Tests
node --run test node --run test
# Build # Build
node --run build node --run build
# To execute a command in a specific package (e.g: packages/utils)
cd packages/utils
node --run test
``` ```
### Production environment with [Docker](https://www.docker.com/) ### Production environment with [Docker](https://www.docker.com/)
```sh ```sh
# Setup and run all the services for you # Setup and run all the services for you
docker compose up --build VERSION=$(git describe --tags) docker compose up --build --detach
``` ```
#### Services started #### Services started
`theoludwig`: <http://127.0.0.1:3000> `theoludwig`: <http://localhost:3000>

View File

@ -1,18 +1,18 @@
<h1 align="center"><a href="https://theoludwig.fr/">Théo LUDWIG</a></h1> <h1 align="center"><a href="https://theoludwig.fr/">Théo LUDWIG</a></h1>
<p align="center"> <p align="center">
<strong>Developer Full Stack • Open-Source Enthusiast</strong> <strong>Developer Full Stack • Open-Source Enthusiast</strong>
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/theoludwig"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a> <a href="https://github.com/theoludwig"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a>
<a href="https://gitlab.com/theoludwig"><img alt="GitLab" src="https://img.shields.io/badge/-GitLab-303030?style=flat&labelColor=303030&logo=gitlab&logoColor=white"/></a> <a href="https://gitlab.com/theoludwig"><img alt="GitLab" src="https://img.shields.io/badge/-GitLab-303030?style=flat&labelColor=303030&logo=gitlab&logoColor=white"/></a>
<a href="https://www.npmjs.com/~theoludwig"><img alt="npm" src="https://img.shields.io/badge/-npm-c4302b?style=flat&labelColor=c4302b&logo=npm&logoColor=white"/></a> <a href="https://www.npmjs.com/~theoludwig"><img alt="npm" src="https://img.shields.io/badge/-npm-c4302b?style=flat&labelColor=c4302b&logo=npm&logoColor=white"/></a>
<a href="https://twitter.com/theoludwig_"><img alt="Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=x&logoColor=white"/></a> <a href="https://twitter.com/theoludwig_"><img alt="Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=x&logoColor=white"/></a>
<a href="https://www.youtube.com/@theo_ludwig"><img alt="YouTube" src="https://img.shields.io/badge/-YouTube-c4302b?style=flat&labelColor=c4302b&logo=youtube&logoColor=white"/></a> <a href="https://www.youtube.com/@theo_ludwig"><img alt="YouTube" src="https://img.shields.io/badge/-YouTube-c4302b?style=flat&labelColor=c4302b&logo=youtube&logoColor=white"/></a>
<a href="https://www.twitch.tv/theoludwig"><img alt="Twitch" src="https://img.shields.io/badge/-Twitch-9147FF?style=flat&labelColor=9147FF&logo=twitch&logoColor=white"/></a> <a href="https://www.twitch.tv/theoludwig"><img alt="Twitch" src="https://img.shields.io/badge/-Twitch-9147FF?style=flat&labelColor=9147FF&logo=twitch&logoColor=white"/></a>
<a href="https://theoludwig.fr/"><img alt="Website" src="https://img.shields.io/badge/-Website-181818?style=flat&labelColor=181818&logo=Google-Chrome&logoColor=white"/></a> <a href="https://theoludwig.fr/"><img alt="Website" src="https://img.shields.io/badge/-Website-181818?style=flat&labelColor=181818&logo=Google-Chrome&logoColor=white"/></a>
<a href="mailto:contact@theoludwig.fr"><img alt="Email" src="https://img.shields.io/badge/-contact@theoludwig.fr-2F7EBE?style=flat&labelColor=2F7EBE&logo=minutemailer&logoColor=white"/></a> <a href="mailto:contact@theoludwig.fr"><img alt="Email" src="https://img.shields.io/badge/-contact@theoludwig.fr-2F7EBE?style=flat&labelColor=2F7EBE&logo=minutemailer&logoColor=white"/></a>
</p> </p>
<hr /> <hr />
@ -21,17 +21,28 @@
```json ```json
{ {
"name": "Théo LUDWIG", "name": "Théo LUDWIG",
"pronouns": "He/Him", "pronouns": "He/Him",
"birthDate": "2003-03-31", "birthDate": "2003-03-31",
"nationality": "Alsace, France", "nationality": "Alsace, France",
"interests": ["Developer Full Stack", "Open-Source Enthusiast"], "interests": ["Developer Full Stack", "Open-Source Enthusiast"],
"skills": { "skills": {
"programmingLanguages": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"], "programmingLanguages": [
"frontend": ["HTML/CSS", "Tailwind CSS", "React.js/Next.js"], "JavaScript/TypeScript",
"backend": ["Laravel", "Node.js", "Fastify", "PostgreSQL"], "Python",
"tools": ["GNU/Linux", "Arch Linux", "Visual Studio Code", "Git", "Docker"] "C/C++",
} "PHP"
],
"frontend": ["HTML/CSS", "Tailwind CSS", "React.js/Next.js"],
"backend": ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
"tools": [
"GNU/Linux",
"Arch Linux",
"Visual Studio Code",
"Git",
"Docker"
]
}
} }
``` ```
@ -40,6 +51,6 @@
## 📈 Statistics ## 📈 Statistics
<p align=center> <p align=center>
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=theoludwig&show_icons=true&theme=dark" alt="Théo LUDWIG's GitHub Stats" /> <img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=theoludwig&show_icons=true&theme=dark" alt="Théo LUDWIG's GitHub Stats" />
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=theoludwig&hide=html,css,javascript&langs_count=8&layout=compact&theme=dark" alt="Théo LUDWIG's Programming Languages" /> <img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=theoludwig&hide=html,css,javascript&langs_count=8&layout=compact&theme=dark" alt="Théo LUDWIG's Programming Languages" />
</p> </p>

View File

@ -1,4 +0,0 @@
{
"root": true,
"extends": ["@repo/eslint-config"]
}

View File

@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View File

@ -7,13 +7,16 @@ const config: StorybookConfig = {
docs: { docs: {
defaultName: "Documentation", defaultName: "Documentation",
}, },
stories: ["../../../packages/**/*.stories.tsx", "../stories/*.mdx"], stories: [
"../../../packages/ui/src/**/*.stories.tsx",
"../../../packages/blog/src/**/*.stories.tsx",
"../stories/*.mdx",
],
addons: [ addons: [
"@chromatic-com/storybook",
"@storybook/addon-essentials", "@storybook/addon-essentials",
"@storybook/addon-storysource", "@storybook/addon-storysource",
"@storybook/addon-a11y", "@storybook/addon-a11y",
"@storybook/addon-links",
"@chromatic-com/storybook",
"@storybook/addon-interactions", "@storybook/addon-interactions",
"storybook-dark-mode", "storybook-dark-mode",
], ],

View File

@ -1,12 +1,18 @@
import "@repo/config-tailwind/styles.css" import "@repo/config-tailwind/styles.css"
import { defaultTranslationValues, Locale } from "@repo/i18n/config" import "./storybook-css-overrides.css"
import i18nMessagesEnglish from "@repo/i18n/translations/en-US.json" import i18nMessages from "@repo/i18n/translations/en-US.json"
import { LOCALE_DEFAULT, TIMEZONE } from "@repo/utils/constants"
import type { Preview } from "@storybook/react" import type { Preview } from "@storybook/react"
import { NextIntlClientProvider } from "next-intl" import { NextIntlClientProvider } from "next-intl"
import { ThemeProvider as NextThemeProvider } from "next-themes" import { ThemeProvider as NextThemeProvider } from "next-themes"
import React from "react" import React from "react"
const preview: Preview = { const preview: Preview = {
globals: {
a11y: {
manual: true,
},
},
parameters: { parameters: {
nextjs: { nextjs: {
appDirectory: true, appDirectory: true,
@ -24,22 +30,21 @@ const preview: Preview = {
stylePreview: true, stylePreview: true,
}, },
controls: { controls: {
disableSaveFromUI: true,
matchers: { matchers: {
color: /(background|color)$/i, color: /(background|color)$/i,
date: /Date$/i, date: /date$/i,
}, },
}, },
}, },
decorators: [ decorators: [
(Story) => { (Story) => {
const locale = "en-US" satisfies Locale
return ( return (
<NextThemeProvider enableColorScheme={false}> <NextThemeProvider enableColorScheme={false}>
<NextIntlClientProvider <NextIntlClientProvider
messages={i18nMessagesEnglish} messages={i18nMessages}
locale={locale} locale={LOCALE_DEFAULT}
defaultTranslationValues={defaultTranslationValues} timeZone={TIMEZONE}
> >
<Story /> <Story />
</NextIntlClientProvider> </NextIntlClientProvider>

View File

@ -0,0 +1,3 @@
body {
overflow: auto !important;
}

View File

@ -5,7 +5,6 @@ import { checkA11y, configureAxe, injectAxe } from "axe-playwright"
/* /*
* See https://storybook.js.org/docs/writing-tests/test-runner#test-hook-api * See https://storybook.js.org/docs/writing-tests/test-runner#test-hook-api
* to learn more about the test-runner hooks API.
*/ */
const config: TestRunnerConfig = { const config: TestRunnerConfig = {
async preVisit(page) { async preVisit(page) {
@ -14,7 +13,8 @@ const config: TestRunnerConfig = {
async postVisit(page, context) { async postVisit(page, context) {
const storyContext = await getStoryContext(page, context) const storyContext = await getStoryContext(page, context)
if (storyContext.parameters?.a11y?.disable) { const isA11yDisabled = storyContext.parameters?.a11y?.disable as boolean
if (isA11yDisabled) {
return return
} }

View File

@ -0,0 +1,13 @@
import typescriptESLint from "typescript-eslint"
import config from "@repo/config-eslint"
export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: typescriptESLint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
})

View File

@ -0,0 +1,84 @@
import http from "node:http"
import fs from "node:fs"
import path from "node:path"
import util from "node:util"
import mime from "mime"
const MIMETYPE_DEFAULT = "application/octet-stream"
const args = util.parseArgs({
options: {
path: { type: "string", default: "public", required: true },
port: { type: "string", default: "3000", required: true },
host: { type: "string", default: "0.0.0.0" },
},
})
const host = args.values.host
const basePath = args.values.path
const port = Number.parseInt(args.values.port, 10)
if (Number.isNaN(port)) {
console.error("Error: Invalid port number.")
process.exit(1)
}
const serverURL = `http://${host}:${port}`
const server = http.createServer(async (request, response) => {
if (request.url == null) {
response.writeHead(400, { "Content-Type": "text/plain" })
response.end("Bad Request")
return
}
const url = new URL(request.url, serverURL)
const urlPath = url.pathname
const filePath = path.join(process.cwd(), basePath, urlPath)
try {
const stat = await fs.promises.stat(filePath)
if (stat.isDirectory()) {
const indexFile = path.join(filePath, "index.html")
try {
const fileContent = await fs.promises.readFile(indexFile)
response.writeHead(200, { "Content-Type": "text/html" })
response.end(fileContent)
} catch {
response.writeHead(403, { "Content-Type": "text/plain" })
response.end("Error: Directory listing not allowed.")
}
} else {
const mimeType = mime.getType(filePath) ?? MIMETYPE_DEFAULT
const fileContent = await fs.promises.readFile(filePath)
response.writeHead(200, { "Content-Type": mimeType })
response.end(fileContent)
}
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
response.writeHead(404, { "Content-Type": "text/plain" })
response.end("Error: File not found.")
} else {
response.writeHead(500, { "Content-Type": "text/plain" })
response.end("Error: Internal Server Error.")
}
}
})
const gracefulShutdown = (): void => {
server.close()
process.exit(0)
}
process.on("SIGTERM", gracefulShutdown)
process.on("SIGINT", gracefulShutdown)
server.listen(
{
host,
port,
},
() => {
console.log(
`HTTP Server is listening at ${util.styleText("cyan", serverURL)}`,
)
console.log(`Serving files from: \`${basePath}\``)
},
)

View File

@ -1,37 +1,39 @@
{ {
"name": "@repo/storybook", "name": "@repo/storybook",
"version": "4.0.0", "version": "0.0.0-develop",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "storybook build", "build": "storybook build",
"dev": "storybook dev --port 6006 --no-open", "dev": "storybook dev --port 6006 --no-open",
"start": "http-server \"storybook-static\" --port 6006 --silent", "start": "node --experimental-strip-types http-server.ts --path=storybook-static --port=6006",
"test": "start-server-and-test \"dev\" http://127.0.0.1:6006 \"test:storybook\"", "test": "start-server-and-test \"start\" http://localhost:6006 \"test:storybook\"",
"test:storybook": "test-storybook", "test:dev": "start-server-and-test \"dev\" http://localhost:6006 \"test:storybook\"",
"test:storybook-coverage": "test-storybook --coverage", "test:storybook": "test-storybook --testTimeout=60000 --maxWorkers=2",
"chromatic": "chromatic" "chromatic": "chromatic"
}, },
"dependencies": { "dependencies": {
"@repo/config-tailwind": "workspace:*", "@repo/config-tailwind": "workspace:*",
"@repo/i18n": "workspace:*", "@repo/i18n": "workspace:*",
"@repo/ui": "workspace:*", "@repo/ui": "workspace:*",
"@repo/utils": "workspace:*",
"@repo/blog": "workspace:*", "@repo/blog": "workspace:*",
"next": "catalog:", "next": "catalog:",
"next-intl": "catalog:", "next-intl": "catalog:",
"next-themes": "catalog:", "next-themes": "catalog:",
"react": "catalog:", "react": "catalog:",
"react-dom": "catalog:" "react-dom": "catalog:",
"mime": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@repo/eslint-config": "workspace:*", "@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@chromatic-com/storybook": "catalog:", "@chromatic-com/storybook": "catalog:",
"@playwright/test": "catalog:", "@playwright/test": "catalog:",
"@storybook/addon-a11y": "catalog:",
"@storybook/addon-essentials": "catalog:", "@storybook/addon-essentials": "catalog:",
"@storybook/addon-interactions": "catalog:",
"@storybook/addon-links": "catalog:",
"@storybook/addon-storysource": "catalog:", "@storybook/addon-storysource": "catalog:",
"@storybook/addon-a11y": "catalog:",
"@storybook/addon-interactions": "catalog:",
"@storybook/addon-themes": "catalog:", "@storybook/addon-themes": "catalog:",
"@storybook/blocks": "catalog:", "@storybook/blocks": "catalog:",
"@storybook/nextjs": "catalog:", "@storybook/nextjs": "catalog:",
@ -44,12 +46,13 @@
"axe-playwright": "catalog:", "axe-playwright": "catalog:",
"chromatic": "catalog:", "chromatic": "catalog:",
"eslint": "catalog:", "eslint": "catalog:",
"http-server": "catalog:",
"start-server-and-test": "catalog:", "start-server-and-test": "catalog:",
"storybook": "catalog:", "storybook": "catalog:",
"storybook-dark-mode": "catalog:", "storybook-dark-mode": "catalog:",
"postcss": "catalog:", "postcss": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
"@tailwindcss/postcss": "catalog:",
"typescript-eslint": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
} }
} }

View File

@ -1,7 +0,0 @@
const config = {
plugins: {
tailwindcss: {},
},
}
export default config

View File

@ -6,7 +6,7 @@ import tailwindConfig from "@repo/config-tailwind"
<Title>Colors</Title> <Title>Colors</Title>
<ColorPalette> <ColorPalette>
{Object.entries(tailwindConfig.theme.colors).map( {Object.entries(tailwindConfig.theme.extend.colors).map(
([colorName, colorValue]) => { ([colorName, colorValue]) => {
const colors = {} const colors = {}

View File

@ -2,7 +2,6 @@ import sharedConfig from "@repo/config-tailwind"
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */ /** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
const config = { const config = {
content: [".storybook/preview.tsx", "../../packages/**/*.tsx"],
presets: [sharedConfig], presets: [sharedConfig],
} }

View File

@ -0,0 +1,7 @@
{
"extends": "@repo/config-typescript/tsconfig.json",
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ESNext"]
},
"include": ["http-server.ts", "./.storybook/**/*.ts", "./.storybook/**/*.tsx"]
}

View File

@ -0,0 +1,9 @@
{
"$schema": "../../node_modules/turbo/schema.json",
"extends": ["//"],
"tasks": {
"test": {
"dependsOn": ["^test", "build"]
}
}
}

View File

@ -1,3 +1,4 @@
TZ=Europe/Paris
HOSTNAME=0.0.0.0 HOSTNAME=0.0.0.0
PORT=3000 PORT=3000
NEXT_TELEMETRY_DISABLED=1 NEXT_TELEMETRY_DISABLED=1

View File

@ -1,15 +0,0 @@
{
"root": true,
"extends": ["@repo/eslint-config/nextjs/.eslintrc.json"],
"ignorePatterns": ["public/"],
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"plugins": ["@typescript-eslint"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": true
}
}
]
}

View File

@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View File

@ -1,12 +1,15 @@
FROM node:22.4.1-slim AS node-pnpm FROM node:24.0.0-slim AS node-pnpm
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable RUN npm install --global corepack@0.32.0 && corepack enable
ENV TURBO_TELEMETRY_DISABLED=1
ENV NEXT_TELEMETRY_DISABLED=1
ENV DO_NOT_TRACK=1
WORKDIR /usr/src/app WORKDIR /usr/src/app
FROM node-pnpm AS builder FROM node-pnpm AS builder
RUN pnpm install --global turbo@2.0.10
COPY ./ ./ COPY ./ ./
RUN pnpm install --global turbo@2.5.3
RUN turbo prune @repo/website --docker RUN turbo prune @repo/website --docker
FROM node-pnpm AS installer FROM node-pnpm AS installer
@ -18,12 +21,15 @@ COPY --from=builder /usr/src/app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
COPY --from=builder /usr/src/app/out/full/ ./ COPY --from=builder /usr/src/app/out/full/ ./
COPY turbo.json turbo.json COPY turbo.json turbo.json
ARG VERSION="0.0.0-develop"
RUN pnpm install --global replace-in-files-cli@3.0.0
RUN VERSION_STRIPPED=${VERSION#v} && replace-in-files --regex='version": *"[^"]*' --replacement='"version": "'"$VERSION_STRIPPED"'"' '**/package.json' '!**/node_modules/**'
RUN pnpm --filter=@repo/website... exec turbo run build RUN pnpm --filter=@repo/website... exec turbo run build
FROM node-pnpm AS runner FROM node-pnpm AS runner
ENV NODE_ENV=production ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0 ENV HOSTNAME=0.0.0.0
ENV NEXT_TELEMETRY_DISABLED=1
ENV IS_STANDALONE=true ENV IS_STANDALONE=true
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 applicationrunner RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 applicationrunner

View File

@ -3,20 +3,21 @@ import { notFound } from "next/navigation"
import { getBlogPostBySlug, getBlogPosts } from "@repo/blog" import { getBlogPostBySlug, getBlogPosts } from "@repo/blog"
import { BlogPostUI } from "@repo/blog/BlogPostUI" import { BlogPostUI } from "@repo/blog/BlogPostUI"
import type { Locale } from "@repo/i18n/config" import type { Locale } from "@repo/utils/constants"
import { unstable_setRequestLocale } from "next-intl/server" import { setRequestLocale } from "next-intl/server"
interface BlogPostPageProps { interface BlogPostPageProps {
params: { params: Promise<{
slug: string slug: string
locale: Locale locale: Locale
} }>
} }
export const generateMetadata = async ( export const generateMetadata = async (
props: BlogPostPageProps, props: BlogPostPageProps,
): Promise<Metadata> => { ): Promise<Metadata> => {
const blogPost = await getBlogPostBySlug(props.params.slug) const { slug } = await props.params
const blogPost = await getBlogPostBySlug(slug)
if (blogPost == null) { if (blogPost == null) {
return notFound() return notFound()
} }
@ -50,10 +51,11 @@ export const generateStaticParams = async (): Promise<
const BlogPostPage: React.FC<BlogPostPageProps> = async (props) => { const BlogPostPage: React.FC<BlogPostPageProps> = async (props) => {
const { params } = props const { params } = props
const { locale, slug } = await params
// Enable static rendering // Enable static rendering
unstable_setRequestLocale(params.locale) setRequestLocale(locale)
const blogPost = await getBlogPostBySlug(params.slug) const blogPost = await getBlogPostBySlug(slug)
if (blogPost == null) { if (blogPost == null) {
return notFound() return notFound()
} }

View File

@ -1,14 +1,15 @@
import { getBlogPosts } from "@repo/blog" import { getBlogPosts } from "@repo/blog"
import { BlogPosts } from "@repo/blog/BlogPosts" import { BlogPosts } from "@repo/blog/BlogPosts"
import { LOCALE_DEFAULT, type LocaleProps } from "@repo/i18n/config" import type { LocaleProps } from "@repo/i18n/routing"
import { MainLayout } from "@repo/ui/Layout/MainLayout" import { MainLayout } from "@repo/ui/Layout/MainLayout"
import { import {
Section, Section,
SectionDescription, SectionDescription,
SectionTitle, SectionTitle,
} from "@repo/ui/Layout/Section" } from "@repo/ui/Layout/Section"
import { LOCALE_DEFAULT } from "@repo/utils/constants"
import type { Metadata } from "next" import type { Metadata } from "next"
import { unstable_setRequestLocale } from "next-intl/server" import { setRequestLocale } from "next-intl/server"
const title = "Blog | Théo LUDWIG" const title = "Blog | Théo LUDWIG"
const description = const description =
@ -35,8 +36,9 @@ interface BlogPageProps extends LocaleProps {}
const BlogPage: React.FC<BlogPageProps> = async (props) => { const BlogPage: React.FC<BlogPageProps> = async (props) => {
const { params } = props const { params } = props
const { locale } = await params
// Enable static rendering // Enable static rendering
unstable_setRequestLocale(params.locale) setRequestLocale(locale)
const posts = await getBlogPosts() const posts = await getBlogPosts()

View File

@ -1,18 +1,19 @@
import "@repo/config-tailwind/styles.css" import "@repo/config-tailwind/styles.css"
import type { LocaleProps } from "@repo/i18n/config" import type { LocaleProps } from "@repo/i18n/routing"
import { Footer } from "@repo/ui/Layout/Footer" import { Footer } from "@repo/ui/Layout/Footer"
import { Header } from "@repo/ui/Layout/Header" import { Header } from "@repo/ui/Layout/Header"
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme" import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
import { VERSION } from "@repo/utils/constants" import { VERSION } from "@repo/utils/constants"
import { unstable_setRequestLocale } from "next-intl/server" import { setRequestLocale } from "next-intl/server"
interface MainLayoutProps extends React.PropsWithChildren, LocaleProps {} interface MainLayoutProps extends React.PropsWithChildren, LocaleProps {}
const MainLayout: React.FC<MainLayoutProps> = async (props) => { const MainLayout: React.FC<MainLayoutProps> = async (props) => {
const { children, params } = props const { children, params } = props
const { locale } = await params
// Enable static rendering // Enable static rendering
unstable_setRequestLocale(params.locale) setRequestLocale(locale)
return ( return (
<ThemeProvider> <ThemeProvider>

View File

@ -1,4 +1,4 @@
import type { LocaleProps } from "@repo/i18n/config" import type { LocaleProps } from "@repo/i18n/routing"
import { About } from "@repo/ui/Home/About" import { About } from "@repo/ui/Home/About"
import { Interests } from "@repo/ui/Home/Interests" import { Interests } from "@repo/ui/Home/Interests"
import { OpenSource } from "@repo/ui/Home/OpenSource" import { OpenSource } from "@repo/ui/Home/OpenSource"
@ -6,15 +6,16 @@ import { Portfolio } from "@repo/ui/Home/Portfolio"
import { Skills } from "@repo/ui/Home/Skills" import { Skills } from "@repo/ui/Home/Skills"
import { MainLayout } from "@repo/ui/Layout/MainLayout" import { MainLayout } from "@repo/ui/Layout/MainLayout"
import { RevealFade } from "@repo/ui/Layout/Section" import { RevealFade } from "@repo/ui/Layout/Section"
import { unstable_setRequestLocale } from "next-intl/server" import { setRequestLocale } from "next-intl/server"
interface HomePageProps extends LocaleProps {} interface HomePageProps extends LocaleProps {}
const HomePage: React.FC<HomePageProps> = (props) => { const HomePage: React.FC<HomePageProps> = async (props) => {
const { params } = props const { params } = props
const { locale } = await params
// Enable static rendering // Enable static rendering
unstable_setRequestLocale(params.locale) setRequestLocale(locale)
return ( return (
<MainLayout> <MainLayout>

View File

@ -1,7 +1,7 @@
import "@repo/config-tailwind/styles.css" import "@repo/config-tailwind/styles.css"
import type { LocaleProps } from "@repo/i18n/config" import type { LocaleProps } from "@repo/i18n/routing"
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme" import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
import { unstable_setRequestLocale } from "next-intl/server" import { setRequestLocale } from "next-intl/server"
interface CurriculumVitaeLayoutProps interface CurriculumVitaeLayoutProps
extends React.PropsWithChildren, extends React.PropsWithChildren,
@ -12,8 +12,9 @@ const CurriculumVitaeLayout: React.FC<CurriculumVitaeLayoutProps> = async (
) => { ) => {
const { children, params } = props const { children, params } = props
const { locale } = await params
// Enable static rendering // Enable static rendering
unstable_setRequestLocale(params.locale) setRequestLocale(locale)
return <ThemeProvider forcedTheme="light">{children}</ThemeProvider> return <ThemeProvider forcedTheme="light">{children}</ThemeProvider>
} }

View File

@ -1,14 +1,15 @@
import type { LocaleProps } from "@repo/i18n/config" import type { LocaleProps } from "@repo/i18n/routing"
import { CurriculumVitae } from "@repo/ui/CurriculumVitae" import { CurriculumVitae } from "@repo/ui/CurriculumVitae"
import { unstable_setRequestLocale } from "next-intl/server" import { setRequestLocale } from "next-intl/server"
interface CurriculumVitaeProps extends LocaleProps {} interface CurriculumVitaeProps extends LocaleProps {}
const CurriculumVitaePage: React.FC<CurriculumVitaeProps> = (props) => { const CurriculumVitaePage: React.FC<CurriculumVitaeProps> = async (props) => {
const { params } = props const { params } = props
const { locale } = await params
// Enable static rendering // Enable static rendering
unstable_setRequestLocale(params.locale) setRequestLocale(locale)
return <CurriculumVitae /> return <CurriculumVitae />
} }

View File

@ -1,23 +1,32 @@
import "@repo/config-tailwind/styles.css" import "@repo/config-tailwind/styles.css"
import type { Locale, LocaleProps } from "@repo/i18n/config" import type { LocaleProps } from "@repo/i18n/routing"
import { LOCALES } from "@repo/i18n/config" import type { Locale } from "@repo/utils/constants"
import type { Metadata } from "next" import { LOCALES } from "@repo/utils/constants"
import type { Metadata, Viewport } from "next"
import { NextIntlClientProvider } from "next-intl" import { NextIntlClientProvider } from "next-intl"
import { import {
getMessages, getMessages,
getTranslations, getTranslations,
unstable_setRequestLocale, setRequestLocale,
} from "next-intl/server" } from "next-intl/server"
import Script from "next/script"
const DOMAIN = "theoludwig.fr"
export const viewport: Viewport = {
themeColor: "#00aeff",
}
export const generateMetadata = async ({ export const generateMetadata = async ({
params, params,
}: LocaleProps): Promise<Metadata> => { }: LocaleProps): Promise<Metadata> => {
const t = await getTranslations({ locale: params.locale }) const { locale } = await params
const t = await getTranslations({ locale })
const title = t("meta.title") const title = t("meta.title")
const description = `${title} - ${t("meta.description")}` const description = `${title} - ${t("meta.description")}`
const image = "/images/logo.webp" const image = "/images/logo.webp"
const url = new URL("https://theoludwig.fr") const url = new URL(`https://${DOMAIN}`)
const locale = LOCALES.join(", ") const locales = LOCALES.join(", ")
return { return {
title, title,
@ -35,7 +44,7 @@ export const generateMetadata = async ({
height: 96, height: 96,
}, },
], ],
locale, locale: locales,
type: "website", type: "website",
}, },
twitter: { twitter: {
@ -60,17 +69,24 @@ interface LocaleLayoutProps extends React.PropsWithChildren, LocaleProps {}
const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => { const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => {
const { children, params } = props const { children, params } = props
const { locale } = await params
// Enable static rendering // Enable static rendering
unstable_setRequestLocale(params.locale) setRequestLocale(locale)
const messages = await getMessages() const messages = await getMessages()
return ( return (
<html lang={params.locale} suppressHydrationWarning> <html lang={locale} suppressHydrationWarning>
<body> <body>
<NextIntlClientProvider messages={messages}> <NextIntlClientProvider messages={messages}>
{children} {children}
</NextIntlClientProvider> </NextIntlClientProvider>
<Script
defer
data-domain={DOMAIN}
src="https://plausible.theoludwig.fr/js/script.js"
/>
</body> </body>
</html> </html>
) )

View File

@ -0,0 +1,13 @@
import typescriptESLint from "typescript-eslint"
import configNextjs from "@repo/config-eslint/nextjs"
export default typescriptESLint.config(...configNextjs, {
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: typescriptESLint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
})

View File

@ -1,3 +0,0 @@
import i18nConfig from "@repo/i18n/i18n"
export default i18nConfig

View File

@ -0,0 +1,3 @@
import i18nRequestConfig from "@repo/i18n/request"
export default i18nRequestConfig

View File

@ -1,25 +1,26 @@
import { LOCALES, LOCALE_DEFAULT, LOCALE_PREFIX } from "@repo/i18n/config" import { routing } from "@repo/i18n/routing"
import createMiddleware from "next-intl/middleware" import createIntlMiddleware from "next-intl/middleware"
export default createMiddleware({ const intlMiddleware = createIntlMiddleware(routing)
locales: LOCALES,
defaultLocale: LOCALE_DEFAULT, export default intlMiddleware
localePrefix: LOCALE_PREFIX,
})
export const config = { export const config = {
matcher: [ matcher: [
// Enable a redirect to a matching locale at the root // Enable a redirect to a matching locale at the root
"/", "/",
// Set a cookie to remember the previous locale for
// all requests that have a locale prefix
// Next.js issue, middleware matcher should support template literals: // Next.js issue, middleware matcher should support template literals:
// https://github.com/vercel/next.js/issues/56398 // https://github.com/vercel/next.js/issues/56398
"/(en-US|fr-FR)/:path*", "/(en-US|fr-FR)/:path*",
// Enable redirects that add missing locales /*
// (e.g. `/pathnames` -> `/en/pathnames`) * Match all request paths except for the ones starting with:
"/((?!_next|_vercel|.*\\..*).*)", * - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
"/((?!api|_next/static|_next/image|images|favicon.ico).*)",
], ],
} }

View File

@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -5,10 +5,10 @@ const IS_STANDALONE = process.env.IS_STANDALONE === "true"
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
output: IS_STANDALONE ? "standalone" : undefined, output: IS_STANDALONE ? "standalone" : undefined,
images: {
// https://github.com/hashicorp/next-mdx-remote/issues/436#issuecomment-2066971842 unoptimized: true,
transpilePackages: ["next-mdx-remote", "shiki"], },
compress: false,
eslint: { eslint: {
ignoreDuringBuilds: true, ignoreDuringBuilds: true,
}, },

View File

@ -1,16 +1,16 @@
{ {
"name": "@repo/website", "name": "@repo/website",
"version": "4.0.0", "version": "0.0.0-develop",
"private": true, "private": true,
"type": "module", "type": "module",
"imports": { "imports": {
"#*": "./*" "#*": "./*"
}, },
"scripts": { "scripts": {
"dev": "next dev --port 3000 --turbo", "dev": "next dev --port 3000 --turbopack",
"build": "next build", "build": "next build",
"start": "next start --port 3000", "start": "next start --port 3000",
"lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives", "lint:eslint": "eslint . --max-warnings 0",
"lint:typescript": "tsc --noEmit" "lint:typescript": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
@ -19,14 +19,16 @@
"@repo/utils": "workspace:*", "@repo/utils": "workspace:*",
"@repo/i18n": "workspace:*", "@repo/i18n": "workspace:*",
"@repo/ui": "workspace:*", "@repo/ui": "workspace:*",
"@mdx-js/mdx": "catalog:",
"next-mdx-remote": "catalog:",
"shiki": "catalog:",
"next": "catalog:", "next": "catalog:",
"next-intl": "catalog:", "next-intl": "catalog:",
"react": "catalog:", "react": "catalog:",
"react-dom": "catalog:", "react-dom": "catalog:"
"sharp": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@repo/eslint-config": "workspace:*", "@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*", "@repo/config-typescript": "workspace:*",
"@types/node": "catalog:", "@types/node": "catalog:",
"@types/react": "catalog:", "@types/react": "catalog:",
@ -35,6 +37,8 @@
"eslint": "catalog:", "eslint": "catalog:",
"postcss": "catalog:", "postcss": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
"@tailwindcss/postcss": "catalog:",
"typescript-eslint": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
} }
} }

View File

@ -1,7 +0,0 @@
const config = {
plugins: {
tailwindcss: {},
},
}
export default config

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 KiB

View File

@ -2,7 +2,6 @@ import sharedConfig from "@repo/config-tailwind"
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */ /** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
const config = { const config = {
content: ["./**/*.tsx", "../../packages/**/*.tsx"],
presets: [sharedConfig], presets: [sharedConfig],
} }

View File

@ -1,16 +1,9 @@
{ {
"extends": "@repo/config-typescript/tsconfig.json", "extends": "@repo/config-typescript/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"], "lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": ["@total-typescript/ts-reset", "@repo/i18n/messages.d.ts"], "types": ["@total-typescript/ts-reset", "@repo/i18n/messages.d.ts"],
"incremental": true,
"noEmit": true,
"allowJs": true, "allowJs": true,
"jsx": "preserve",
"paths": { "paths": {
"#*": ["./*"] "#*": ["./*"]
}, },

View File

@ -6,6 +6,8 @@ services:
build: build:
context: "./" context: "./"
dockerfile: "./apps/website/Dockerfile" dockerfile: "./apps/website/Dockerfile"
args:
VERSION: ${VERSION-0.0.0-develop}
ports: ports:
- "${WEBSITE_PORT-3000}:${WEBSITE_PORT-3000}" - "${WEBSITE_PORT-3000}:${WEBSITE_PORT-3000}"
environment: environment:

View File

@ -0,0 +1,35 @@
import typescriptESLint from "typescript-eslint"
import configConventions from "eslint-config-conventions"
import importX from "eslint-plugin-import-x"
export default typescriptESLint.config(
{
ignores: [
".next",
"**/next.config.js",
"**/eslint.config.js",
"**/tailwind.config.js",
"**/postcss.config.js",
"**/kysely.config.ts",
],
},
...configConventions,
{
name: "config-eslint",
plugins: {
"import-x": importX,
},
rules: {
"import-x/extensions": [
"error",
"ignorePackages",
{
ts: "always",
tsx: "always",
js: "never",
jsx: "never",
},
],
},
},
)

7
configs/config-eslint/index.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import type typescriptESLint from "typescript-eslint"
declare const eslintConfigConventions: ReturnType<
typeof typescriptESLint.config
>
export default eslintConfigConventions

View File

@ -0,0 +1,57 @@
import { FlatCompat } from "@eslint/eslintrc"
import typescriptESLint from "typescript-eslint"
import config from "../eslint.config.js"
const flatCompat = new FlatCompat({
baseDirectory: import.meta.dirname,
})
export default typescriptESLint.config(
...config,
...flatCompat.extends("next/core-web-vitals"),
{
name: "config-eslint/nextjs",
settings: {
tailwindcss: {
callees: ["classNames", "cva"],
},
react: {
version: "detect",
},
},
rules: {
"@next/next/no-html-link-for-pages": "off",
"@next/next/no-img-element": "off",
"react/self-closing-comp": [
"error",
{
component: true,
html: true,
},
],
"react/void-dom-elements-no-children": "error",
"react/jsx-boolean-value": "error",
"no-restricted-imports": [
"error",
{
paths: [
{
name: "next/link",
message: "Please import from `@repo/i18n/routing` instead.",
},
{
name: "next/navigation",
importNames: [
"redirect",
"permanentRedirect",
"useRouter",
"usePathname",
],
message: "Please import from `@repo/i18n/routing` instead.",
},
],
},
],
},
},
)

View File

@ -0,0 +1,32 @@
{
"name": "@repo/config-eslint",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./eslint.config.js",
"require": "./eslint.config.js",
"default": "./eslint.config.js"
},
"./nextjs": {
"types": "./index.d.ts",
"import": "./nextjs/eslint.config.js",
"require": "./nextjs/eslint.config.js",
"default": "./nextjs/eslint.config.js"
}
},
"devDependencies": {
"@eslint/eslintrc": "catalog:",
"typescript-eslint": "catalog:",
"eslint": "catalog:",
"eslint-config-conventions": "catalog:",
"eslint-plugin-promise": "catalog:",
"eslint-plugin-unicorn": "catalog:",
"eslint-config-next": "catalog:",
"eslint-plugin-import-x": "catalog:",
"typescript": "catalog:",
"globals": "catalog:"
}
}

View File

@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View File

@ -1,4 +1,5 @@
import { type ClassValue, clsx } from "clsx" import type { ClassValue } from "clsx"
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
export const classNames = (...inputs: ClassValue[]): string => { export const classNames = (...inputs: ClassValue[]): string => {

View File

@ -0,0 +1,13 @@
import typescriptESLint from "typescript-eslint"
import config from "@repo/config-eslint"
export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: typescriptESLint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
})

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/config-tailwind", "name": "@repo/config-tailwind",
"version": "4.0.0", "version": "0.0.0-develop",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./tailwind.config.js", "main": "./tailwind.config.js",
@ -21,11 +21,13 @@
"tailwind-merge": "catalog:" "tailwind-merge": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@repo/eslint-config": "workspace:*", "@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*", "@repo/config-typescript": "workspace:*",
"@tailwindcss/typography": "catalog:", "@tailwindcss/typography": "catalog:",
"typescript-eslint": "catalog:",
"eslint": "catalog:", "eslint": "catalog:",
"postcss": "catalog:", "postcss": "catalog:",
"tailwindcss": "catalog:" "tailwindcss": "catalog:",
"@tailwindcss/postcss": "catalog:"
} }
} }

View File

@ -1,14 +1,28 @@
@import "@fontsource/montserrat/400.css"; @import "tailwindcss";
@import "@fontsource/montserrat/500.css"; @config "./tailwind.config.js";
@import "@fontsource/montserrat/600.css";
@import "@fontsource/montserrat/700.css";
@import "@fontsource/montserrat/800.css";
@tailwind base; @source "../../packages/ui";
@tailwind components; @source "../../packages/blog";
@tailwind utilities; @source "../../apps/website";
@source "../../apps/storybook/.storybook";
@import "@fontsource/montserrat/400.css" layer(base);
@import "@fontsource/montserrat/500.css" layer(base);
@import "@fontsource/montserrat/600.css" layer(base);
@import "@fontsource/montserrat/700.css" layer(base);
@import "@fontsource/montserrat/800.css" layer(base);
@layer base { @layer base {
[type="search"]::-webkit-search-decoration,
[type="search"]::-webkit-search-cancel-button {
appearance: none;
}
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
b, b,
strong { strong {
@apply font-semibold; @apply font-semibold;
@ -36,11 +50,11 @@
@apply italic tracking-wider; @apply italic tracking-wider;
} }
blockquote { blockquote {
@apply border-gray border-l-4 pl-3 italic; @apply border-gray-lighter border-l-4 pl-3 italic;
} }
kbd { kbd {
@apply bg-gray rounded-md px-2 dark:text-black; @apply bg-gray-lighter rounded-md px-2 dark:text-black;
} }
mark { mark {
@ -82,7 +96,7 @@ body {
} }
.prose { .prose {
@apply dark:text-gray !max-w-5xl scroll-smooth text-black; @apply dark:text-gray-lighter !max-w-5xl scroll-smooth text-black;
} }
.prose p { .prose p {
@ -107,7 +121,7 @@ body {
} }
.prose strong { .prose strong {
@apply dark:text-gray text-black; @apply dark:text-gray-lighter text-black;
} }
.prose h2, .prose h2,
@ -151,10 +165,6 @@ code .line::before {
word-break: normal; word-break: normal;
} }
code .line:last-child {
display: none;
}
.katex .base { .katex .base {
display: inline !important; display: inline !important;
white-space: normal !important; white-space: normal !important;

View File

@ -0,0 +1,51 @@
import typographyPlugin from "@tailwindcss/typography"
/** @type {Omit<import('tailwindcss').Config, "content">} */
const config = {
darkMode: "selector",
theme: {
extend: {
colors: {
primary: {
DEFAULT: "#0056b3",
dark: "#00aeff",
},
background: {
DEFAULT: "#fff",
dark: "#181818",
},
"gray-lighter": "#d1d5db",
"gray-darker": {
DEFAULT: "#4b5563",
dark: "#9ca3af",
},
yellow: "#fef08a",
},
boxShadow: {
dark: "0px 0px 2px 2px rgba(0, 0, 0, 0.25)",
light: "0px 0px 2px 2px rgba(0, 0, 0, 0.10)",
darkFlag: "0px 1px 10px hsla(0, 0%, 100%, 0.2)",
lightFlag: "0px 1px 10px rgba(0, 0, 0, 0.25)",
},
fontFamily: {
sans: ["Montserrat", "sans-serif"],
},
typography: {
DEFAULT: {
css: {
a: {
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
fontWeight: 400,
},
},
},
},
},
},
},
plugins: [typographyPlugin],
}
export default config

View File

@ -0,0 +1,6 @@
{
"extends": "@repo/config-typescript/tsconfig.json",
"compilerOptions": {
"lib": ["ESNext"]
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/config-typescript", "name": "@repo/config-typescript",
"version": "4.0.0", "version": "0.0.0-develop",
"private": true, "private": true,
"files": [ "files": [
"tsconfig.json" "tsconfig.json"

View File

@ -6,15 +6,25 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noImplicitOverride": true, "noImplicitOverride": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noPropertyAccessFromIndexSignature": true, "noImplicitThis": true,
"noImplicitAny": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowImportingTsExtensions": true,
"skipLibCheck": true, "skipLibCheck": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true "incremental": true,
"noEmit": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true
} }
} }

View File

@ -1,39 +1,32 @@
{ {
"name": "repo", "name": "repo",
"version": "4.0.0", "version": "0.0.0-develop",
"private": true, "private": true,
"type": "module", "type": "module",
"packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903", "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
"engines": { "engines": {
"node": ">=22.0.0", "node": ">=24.0.0"
"pnpm": ">=9.5.0"
}, },
"scripts": { "scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel", "dev": "turbo run dev --parallel",
"start": "turbo run start --parallel", "start": "turbo run start --parallel",
"build": "turbo run build",
"test": "turbo run test", "test": "turbo run test",
"lint:editorconfig": "editorconfig-checker", "lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint-cli2", "lint:markdown": "markdownlint-cli2",
"lint:prettier": "prettier . --check",
"lint:eslint": "turbo run lint:eslint",
"lint:typescript": "turbo run lint:typescript", "lint:typescript": "turbo run lint:typescript",
"release": "semantic-release" "lint:eslint": "turbo run lint:eslint",
"lint:prettier": "prettier . --check"
}, },
"devDependencies": { "devDependencies": {
"@saithodev/semantic-release-backmerge": "4.0.1", "editorconfig-checker": "catalog:",
"@semantic-release/exec": "6.0.3",
"@semantic-release/git": "10.0.1",
"editorconfig-checker": "5.1.8",
"playwright": "catalog:", "playwright": "catalog:",
"prettier": "3.3.3", "prettier": "catalog:",
"prettier-plugin-tailwindcss": "0.6.5", "prettier-plugin-tailwindcss": "catalog:",
"markdownlint-cli2": "0.13.0", "markdownlint-cli2": "catalog:",
"markdownlint": "0.34.0", "markdownlint": "catalog:",
"markdownlint-rule-relative-links": "3.0.0", "markdownlint-rule-relative-links": "catalog:",
"replace-in-files-cli": "3.0.0", "turbo": "catalog:",
"semantic-release": "23.1.1",
"turbo": "2.0.10",
"typescript": "catalog:" "typescript": "catalog:"
} }
} }

View File

@ -1,14 +0,0 @@
{
"root": true,
"extends": ["@repo/eslint-config/nextjs/.eslintrc.json"],
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"plugins": ["@typescript-eslint"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": true
}
}
]
}

View File

@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View File

@ -0,0 +1,13 @@
import typescriptESLint from "typescript-eslint"
import configNextjs from "@repo/config-eslint/nextjs"
export default typescriptESLint.config(...configNextjs, {
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: typescriptESLint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
})

View File

@ -1,6 +1,6 @@
{ {
"name": "@repo/blog", "name": "@repo/blog",
"version": "4.0.0", "version": "0.0.0-develop",
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {
@ -9,7 +9,7 @@
"./BlogPostUI": "./src/BlogPostUI.tsx" "./BlogPostUI": "./src/BlogPostUI.tsx"
}, },
"scripts": { "scripts": {
"lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives", "lint:eslint": "eslint src --max-warnings 0",
"lint:typescript": "tsc --noEmit" "lint:typescript": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
@ -37,7 +37,7 @@
"react-icons": "catalog:" "react-icons": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@repo/eslint-config": "workspace:*", "@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*", "@repo/config-typescript": "workspace:*",
"@types/node": "catalog:", "@types/node": "catalog:",
"@types/react": "catalog:", "@types/react": "catalog:",
@ -49,6 +49,8 @@
"eslint": "catalog:", "eslint": "catalog:",
"postcss": "catalog:", "postcss": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
"@tailwindcss/postcss": "catalog:",
"typescript-eslint": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
} }
} }

View File

@ -1,7 +0,0 @@
const config = {
plugins: {
tailwindcss: {},
},
}
export default config

View File

@ -109,11 +109,11 @@ const transaction = charge(user, subscription)
```typescript ```typescript
interface Car { interface Car {
carModel: string carModel: string
carColor: "red" | "blue" | "yellow" carColor: "red" | "blue" | "yellow"
} }
const printCar = (car: Car): void => { const printCar = (car: Car): void => {
console.log(`${car.carModel} (${car.carColor})`) console.log(`${car.carModel} (${car.carColor})`)
} }
``` ```
@ -121,11 +121,11 @@ const printCar = (car: Car): void => {
```typescript ```typescript
interface Car { interface Car {
model: string model: string
color: "red" | "blue" | "yellow" color: "red" | "blue" | "yellow"
} }
const printCar = (car: Car): void => { const printCar = (car: Car): void => {
console.log(`${car.model} (${car.color})`) console.log(`${car.model} (${car.color})`)
} }
``` ```
@ -174,13 +174,13 @@ import fs from "node:fs"
import path from "node:path" import path from "node:path"
const createFile = async ( const createFile = async (
name: string, name: string,
isTemporary: boolean = false, isTemporary: boolean = false,
): Promise<void> => { ): Promise<void> => {
if (isTemporary) { if (isTemporary) {
return await fs.promises.writeFile(path.join("temporary", name), "") return await fs.promises.writeFile(path.join("temporary", name), "")
} }
return await fs.promises.writeFile(name, "") return await fs.promises.writeFile(name, "")
} }
``` ```
@ -193,11 +193,11 @@ import fs from "node:fs"
import path from "node:path" import path from "node:path"
const createFile = async (name: string): Promise<void> => { const createFile = async (name: string): Promise<void> => {
await fs.promises.writeFile(name, "") await fs.promises.writeFile(name, "")
} }
const createTemporaryFile = async (name: string): Promise<void> => { const createTemporaryFile = async (name: string): Promise<void> => {
await createFile(path.join("temporary", name)) await createFile(path.join("temporary", name))
} }
``` ```

View File

@ -40,8 +40,8 @@ These configurations are stored in the `.gitconfig` file in your home directory
```sh ```sh
[user] [user]
name = Username name = Username
email = email@example.com email = email@example.com
``` ```
You can find more information and useful `git` configurations in the [official documentation](https://git-scm.com/docs/git-config). You can find more information and useful `git` configurations in the [official documentation](https://git-scm.com/docs/git-config).
@ -88,6 +88,8 @@ git commit -m "Commit message"
# Commit changes in the past # Commit changes in the past
git commit --date "10 day ago" -m "Commit message" git commit --date "10 day ago" -m "Commit message"
# Also update the committer date for the last commit
git filter-branch --env-filter 'export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"' HEAD^..HEAD
# Add remote repository # Add remote repository
git remote add <remote> <url> git remote add <remote> <url>
@ -266,7 +268,7 @@ Sometimes, you want to compare what commits have been made between two branches,
```sh ```sh
[alias] [alias]
diff-commits = !sh -c 'echo -n "Commits in $2 not in $1 \\(" && printf "%d" $(git cherry -v $1 $2 | wc -l) && echo "\\)" && git cherry -v $1 $2 && echo "" && echo -n "Commits in $1 not in $2 \\(" && printf "%d" $(git cherry -v $2 $1 | wc -l) && echo "\\)" && git cherry -v $2 $1' - diff-commits = !sh -c 'echo -n "Commits in $2 not in $1 \\(" && printf "%d" $(git cherry -v $1 $2 | wc -l) && echo "\\)" && git cherry -v $1 $2 && echo "" && echo -n "Commits in $1 not in $2 \\(" && printf "%d" $(git cherry -v $2 $1 | wc -l) && echo "\\)" && git cherry -v $2 $1' -
``` ```
With this alias, we can compare the commits between `main` and `develop` branches for example: With this alias, we can compare the commits between `main` and `develop` branches for example:

View File

@ -39,23 +39,23 @@ The code of this website is open source on [GitHub](https://github.com/theoludwi
- [Next.js](https://nextjs.org/) - [Next.js](https://nextjs.org/)
It allows to have a server-side rendered website, that means that it is faster and easier to have a good <abbr title="Search Engine Optimization">SEO</abbr> than a <abbr title="Single Page Application">SPA</abbr>. It allows to have a server-side rendered website, that means that it is faster and easier to have a good <abbr title="Search Engine Optimization">SEO</abbr> than a <abbr title="Single Page Application">SPA</abbr>.
- [MDX](https://mdxjs.com/) - [MDX](https://mdxjs.com/)
MDX is an extension of Markdown that allows you to use custom React components. MDX is an extension of Markdown that allows you to use custom React components.
Here's what Markdown looks like: Here's what Markdown looks like:
```md ```md
A simple paragraph, with some **bold** text and some `inline code`. A simple paragraph, with some **bold** text and some `inline code`.
``` ```
When using Markdown in a web application, there's a "compile" step; the Markdown needs to be transformed into HTML, so that it can be understood by the browser. Those asterisks get turned into a `<strong>` tag, and each paragraph gets a `<p>` tag etc. When using Markdown in a web application, there's a "compile" step; the Markdown needs to be transformed into HTML, so that it can be understood by the browser. Those asterisks get turned into a `<strong>` tag, and each paragraph gets a `<p>` tag etc.
- [Tailwind CSS](https://tailwindcss.com/) - [Tailwind CSS](https://tailwindcss.com/)
Tailwind is a CSS framework to rapidly build modern websites without ever leaving HTML. Tailwind is a CSS framework to rapidly build modern websites without ever leaving HTML.
## Conclusion ## Conclusion

View File

@ -9,9 +9,9 @@ import rehypeSlug from "rehype-slug"
import remarkGfm from "remark-gfm" import remarkGfm from "remark-gfm"
import remarkMath from "remark-math" import remarkMath from "remark-math"
import { Link } from "@repo/i18n/navigation" import { Link } from "@repo/i18n/routing"
import "katex/dist/katex.min.css" import "katex/dist/katex.min.css"
import { BlogPostComments } from "./BlogPostComments" import { BlogPostComments } from "./BlogPostComments.tsx"
const Heading: React.FC< const Heading: React.FC<
React.DetailedHTMLProps< React.DetailedHTMLProps<
@ -103,13 +103,14 @@ export const BlogPostContent: React.FC<BlogPostContentProps> = async (
}, },
a: (props) => { a: (props) => {
const { href = "", ...rest } = props const { href = "", ...rest } = props
if (href.startsWith("#")) { const hrefString = href as string
if (hrefString.startsWith("#")) {
return <a {...props} /> return <a {...props} />
} }
if (href.startsWith("../posts/")) { if (hrefString.startsWith("../posts/")) {
return ( return (
<Link <Link
href={href href={hrefString
.replace("../posts/", "/blog/") .replace("../posts/", "/blog/")
.replace(".md", "")} .replace(".md", "")}
{...rest} {...rest}

View File

@ -3,8 +3,8 @@ import "katex/dist/katex.min.css"
import { Typography } from "@repo/ui/Design/Typography" import { Typography } from "@repo/ui/Design/Typography"
import { MainLayout } from "@repo/ui/Layout/MainLayout" import { MainLayout } from "@repo/ui/Layout/MainLayout"
import type { BlogPost } from "./BlogPost" import type { BlogPost } from "./BlogPost.tsx"
import { BlogPostContent } from "./BlogPostContent" import { BlogPostContent } from "./BlogPostContent.tsx"
export interface BlogPostUIProps { export interface BlogPostUIProps {
blogPost: BlogPost blogPost: BlogPost

View File

@ -1,8 +1,8 @@
import { Link } from "@repo/i18n/navigation" import { Link } from "@repo/i18n/routing"
import { Typography } from "@repo/ui/Design/Typography" import { Typography } from "@repo/ui/Design/Typography"
import { Section, SectionContent } from "@repo/ui/Layout/Section" import { Section, SectionContent } from "@repo/ui/Layout/Section"
import { getISODate } from "@repo/utils/dates" import { getISODate } from "@repo/utils/dates"
import type { BlogPost } from "./BlogPost" import type { BlogPost } from "./BlogPost.tsx"
export interface BlogPostsProps { export interface BlogPostsProps {
posts: BlogPost[] posts: BlogPost[]

View File

@ -2,7 +2,7 @@ import fs from "node:fs"
import path from "node:path" import path from "node:path"
import matter from "gray-matter" import matter from "gray-matter"
import type { BlogPost, FrontMatter } from "./BlogPost" import type { BlogPost, FrontMatter } from "./BlogPost.tsx"
export const BLOG_POSTS_PATH = path.join( export const BLOG_POSTS_PATH = path.join(
process.cwd(), process.cwd(),

View File

@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react" import type { Meta, StoryObj } from "@storybook/react"
import { BLOG_POST_MOCK } from "../BlogPost" import { BLOG_POST_MOCK } from "../BlogPost.ts"
import { BlogPostUI as BlogPostUIComponent } from "../BlogPostUI" import { BlogPostUI as BlogPostUIComponent } from "../BlogPostUI.tsx"
const meta = { const meta = {
title: "Blog/BlogPostUI", title: "Blog/BlogPostUI",

View File

@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react" import type { Meta, StoryObj } from "@storybook/react"
import { BLOG_POST_MOCK } from "../BlogPost" import { BLOG_POST_MOCK } from "../BlogPost.ts"
import { BlogPosts as BlogPostsComponent } from "../BlogPosts" import { BlogPosts as BlogPostsComponent } from "../BlogPosts.tsx"
const meta = { const meta = {
title: "Blog/BlogPosts", title: "Blog/BlogPosts",

View File

@ -2,7 +2,6 @@ import sharedConfig from "@repo/config-tailwind"
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */ /** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
const config = { const config = {
content: ["./src/**/*.tsx"],
presets: [sharedConfig], presets: [sharedConfig],
} }

View File

@ -1,18 +1,11 @@
{ {
"extends": "@repo/config-typescript/tsconfig.json", "extends": "@repo/config-typescript/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"], "lib": ["DOM", "DOM.Iterable", "ESNext"],
"types": [ "types": [
"@total-typescript/ts-reset", "@total-typescript/ts-reset",
"@repo/i18n/messages.d.ts", "@types/node",
"@types/node" "@repo/i18n/messages.d.ts"
], ]
"jsx": "preserve",
"noEmit": true
} }
} }

View File

@ -1,3 +0,0 @@
{
"extends": ["conventions"]
}

View File

@ -1,57 +0,0 @@
{
"extends": [
"conventions",
"next/core-web-vitals",
"plugin:tailwindcss/recommended",
"plugin:storybook/recommended"
],
"ignorePatterns": [
"next.config.js",
"tailwind.config.js",
"postcss.config.js",
"vitest.config.ts"
],
"settings": {
"tailwindcss": {
"callees": ["classNames", "cva"]
},
"react": {
"version": "detect"
}
},
"rules": {
"tailwindcss/classnames-order": "off",
"tailwindcss/no-custom-classname": "off",
"@next/next/no-html-link-for-pages": "off",
"react/self-closing-comp": [
"error",
{
"component": true,
"html": true
}
],
"react/void-dom-elements-no-children": "error",
"react/jsx-boolean-value": "error",
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "next/link",
"message": "Please import from `@repo/i18n/navigation` instead."
},
{
"name": "next/navigation",
"importNames": [
"redirect",
"permanentRedirect",
"useRouter",
"usePathname"
],
"message": "Please import from `@repo/i18n/navigation` instead."
}
]
}
]
}
}

View File

@ -1,22 +0,0 @@
{
"name": "@repo/eslint-config",
"version": "4.0.0",
"private": true,
"main": ".eslintrc.json",
"files": [
".eslintrc.json",
"nextjs/.eslintrc.json"
],
"devDependencies": {
"@typescript-eslint/eslint-plugin": "catalog:",
"@typescript-eslint/parser": "catalog:",
"eslint": "catalog:",
"eslint-config-conventions": "catalog:",
"eslint-plugin-promise": "catalog:",
"eslint-plugin-unicorn": "catalog:",
"eslint-config-next": "catalog:",
"eslint-plugin-storybook": "catalog:",
"eslint-plugin-tailwindcss": "catalog:",
"typescript": "catalog:"
}
}

View File

@ -1,14 +0,0 @@
{
"root": true,
"extends": ["@repo/eslint-config"],
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"plugins": ["@typescript-eslint"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": true
}
}
]
}

View File

@ -1,7 +0,0 @@
const config = {
plugins: {
tailwindcss: {},
},
}
export default config

View File

@ -1,57 +0,0 @@
import typographyPlugin from "@tailwindcss/typography"
import { fontFamily } from "tailwindcss/defaultTheme"
/** @type {Omit<import('tailwindcss').Config, "content">} */
const config = {
darkMode: "class",
theme: {
colors: {
primary: {
DEFAULT: "#0056b3",
dark: "#00aeff",
},
background: {
DEFAULT: "#fff",
dark: "#181818",
},
white: "#fff",
black: "#000",
gray: "#d1d5db",
"gray-darker": {
DEFAULT: "#4b5563",
dark: "#9ca3af",
},
yellow: "#fef08a",
transparent: "transparent",
inherit: "inherit",
current: "currentColor",
},
boxShadow: {
dark: "0px 0px 2px 2px rgba(0, 0, 0, 0.25)",
light: "0px 0px 2px 2px rgba(0, 0, 0, 0.10)",
darkFlag: "0px 1px 10px hsla(0, 0%, 100%, 0.2)",
lightFlag: "0px 1px 10px rgba(0, 0, 0, 0.25)",
},
fontFamily: {
sans: ["'Montserrat'", ...fontFamily.sans],
},
extend: {
typography: {
DEFAULT: {
css: {
a: {
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
fontWeight: 400,
},
},
},
},
},
},
},
plugins: [typographyPlugin],
}
export default config

View File

@ -1,10 +0,0 @@
{
"extends": "@repo/config-typescript/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true
}
}

View File

@ -1,14 +0,0 @@
{
"root": true,
"extends": ["@repo/eslint-config"],
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"plugins": ["@typescript-eslint"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": true
}
}
]
}

View File

@ -0,0 +1,13 @@
import typescriptESLint from "typescript-eslint"
import config from "@repo/config-eslint"
export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"],
languageOptions: {
parser: typescriptESLint.parser,
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
})

View File

@ -1,35 +1,33 @@
{ {
"name": "@repo/i18n", "name": "@repo/i18n",
"version": "4.0.0", "version": "0.0.0-develop",
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {
"./translations/*.json": "./src/translations/*.json", "./translations/*.json": "./src/translations/*.json",
"./config": "./src/config.tsx",
"./i18n": "./src/i18n.ts",
"./messages.d.ts": "./src/messages.d.ts", "./messages.d.ts": "./src/messages.d.ts",
"./navigation": "./src/navigation.ts" "./request": "./src/request.ts",
"./routing": "./src/routing.ts"
}, },
"scripts": { "scripts": {
"lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives", "lint:eslint": "eslint src --max-warnings 0",
"lint:typescript": "tsc --noEmit", "lint:typescript": "tsc --noEmit"
"test": "vitest run"
}, },
"dependencies": { "dependencies": {
"deepmerge": "catalog:", "@repo/utils": "workspace:*",
"next": "catalog:", "next": "catalog:",
"next-intl": "catalog:", "next-intl": "catalog:",
"react": "catalog:", "react": "catalog:",
"react-dom": "catalog:" "react-dom": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@repo/eslint-config": "workspace:*", "@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*", "@repo/config-typescript": "workspace:*",
"@types/react": "catalog:", "@types/react": "catalog:",
"@types/react-dom": "catalog:", "@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:", "@total-typescript/ts-reset": "catalog:",
"eslint": "catalog:", "eslint": "catalog:",
"typescript": "catalog:", "typescript-eslint": "catalog:",
"vitest": "catalog:" "typescript": "catalog:"
} }
} }

View File

@ -1,30 +0,0 @@
import type { RichTranslationValues } from "next-intl"
export const LOCALES = ["en-US", "fr-FR"] as const
export type Locale = (typeof LOCALES)[number]
export const LOCALE_DEFAULT = "en-US" satisfies Locale
export const LOCALE_PREFIX = "never"
export interface LocaleProps {
params: {
locale: Locale
}
}
export const defaultTranslationValues: RichTranslationValues = {
br: () => {
return <br />
},
strong: (children) => {
return <strong>{children}</strong>
},
em: (children) => {
return <em>{children}</em>
},
s: (children) => {
return <s>{children}</s>
},
u: (children) => {
return <u>{children}</u>
},
}

View File

@ -1,27 +0,0 @@
import deepmerge from "deepmerge"
import type { AbstractIntlMessages } from "next-intl"
import { getRequestConfig } from "next-intl/server"
import { notFound } from "next/navigation"
import type { Locale } from "./config"
import { defaultTranslationValues, LOCALE_DEFAULT, LOCALES } from "./config"
export default getRequestConfig(async ({ locale }) => {
if (!LOCALES.includes(locale as Locale)) {
return notFound()
}
const userMessages = (await import(`./translations/${locale}.json`)).default
const defaultMessages = (
await import(`./translations/${LOCALE_DEFAULT}.json`)
).default
const messages = deepmerge<AbstractIntlMessages>(
defaultMessages,
userMessages,
)
return {
messages,
defaultTranslationValues,
}
})

View File

@ -1,10 +1,9 @@
import type en from "./translations/en-US.json" import type { routing } from "./routing.ts"
import type messages from "./translations/en-US.json"
type Messages = typeof en declare module "next-intl" {
interface AppConfig {
declare global { Locale: (typeof routing.locales)[number]
/** Messages: typeof messages
* Use type safe message keys with `next-intl`. }
*/
interface IntlMessages extends Messages {}
} }

View File

@ -1,9 +0,0 @@
import { createSharedPathnamesNavigation } from "next-intl/navigation"
import { LOCALES, LOCALE_PREFIX } from "./config"
export const { Link, redirect, usePathname, useRouter, permanentRedirect } =
createSharedPathnamesNavigation({
locales: LOCALES,
localePrefix: LOCALE_PREFIX,
})

Some files were not shown because too many files have changed in this diff Show More