1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2025-09-11 23:09:22 +02:00

Compare commits

..

45 Commits

Author SHA1 Message Date
92eca82127 fix: update Curriculum Vitae 2025-09-09 19:32:06 +02:00
4502e32250 build(deps): update latest 2025-09-09 19:10:52 +02:00
d6f2d9e3a6 fix: add more details to tasks in Work experiences 2025-09-02 00:25:18 +02:00
eeb5e8872f feat: update skills 2025-09-01 23:34:03 +02:00
7d22880905 fix: update X/Twitter logo 2025-09-01 21:17:17 +02:00
49896bbaeb fix: rename portfolio to projects + reorganize home page ordering of sections 2025-08-31 20:51:55 +02:00
b3140dd977 fix: update Curriculum Vitae + add Fusey interest 2025-08-31 20:38:14 +02:00
39ef3e80ce chore: remove deprecated warning Next.js image un-configured qualities
Ref: https://nextjs.org/docs/messages/next-image-unconfigured-qualities
2025-08-31 19:57:46 +02:00
64a5ca73ad chore: clean up 2025-08-31 19:35:14 +02:00
b16699f5aa build(deps): update latest 2025-05-29 20:58:40 +02:00
e044b4caea build(deps): update Node.js to v24.0.2 to address security issue
Ref: https://nodejs.org/en/blog/release/v24.0.2
2025-05-20 21:13:02 +02:00
d795025860 chore: remove not needed --experimental-strip-types 2025-05-11 18:04:26 +02:00
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
semantic-release-bot
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
semantic-release-bot
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
semantic-release-bot
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
semantic-release-bot
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
semantic-release-bot
6853ac6884 chore(release): 4.0.0 [skip ci] 2024-07-31 22:55:33 +00:00
b199aedf77 feat: add Locale switch to Curriculum Vitae (not visible in print mode) 2024-08-01 00:48:22 +02:00
da4b483a3c fix: a11y issues with curriculum-vitae 2024-08-01 00:36:45 +02:00
012fea869f feat: translate Curriculum Vitae in both English and French 2024-08-01 00:26:46 +02:00
a596d1c443 feat: components structure Curriculum Vitae 2024-07-31 22:27:51 +02:00
b4611e4a7f feat: init Curriculum Vitae 2024-07-31 19:23:14 +02:00
b5c50728de refactor: components struture 2024-07-31 11:41:39 +02:00
ceeeb2f9c5 fix: add missing unstable_setRequestLocale to enable static rendering 2024-07-31 11:00:30 +02:00
237 changed files with 8771 additions and 11679 deletions

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
---
name: "🐛 Bug Report"
about: "Report an unexpected problem or unintended behavior."
title: "[Bug]"
labels: "bug"
---
<!--
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.
-->
## Steps To Reproduce
1. Step 1
2. Step 2
## The current behavior
## The expected behavior

View File

@@ -1,18 +0,0 @@
---
name: "📜 Documentation"
about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)."
title: "[Documentation]"
labels: "documentation"
---
<!-- Please make sure your issue has not already been fixed. -->
## Documentation
<!-- Please uncomment the type of documentation problem this issue address -->
<!-- Documentation is Missing -->
<!-- Documentation is Confusing -->
<!-- Documentation has Typo errors -->
## Proposal

View File

@@ -1,20 +0,0 @@
---
name: "✨ Feature Request"
about: "Suggest a new feature idea."
title: "[Feature]"
labels: "feature request"
---
<!-- Please make sure your issue has not already been fixed. -->
## Description
<!-- A clear and concise description of the problem or missing capability... -->
## Describe the solution you'd like
<!-- If you have a solution in mind, please describe it. -->
## Describe alternatives you've considered
<!-- Have you considered any alternative solutions or workarounds? -->

View File

@@ -1,20 +0,0 @@
---
name: "🔧 Improvement"
about: "Improve structure/format/performance/refactor/tests of the code."
title: "[Improvement]"
labels: "improvement"
---
<!-- Please make sure your issue has not already been fixed. -->
## Type of Improvement
<!-- Please uncomment the type of improvements this issue address -->
<!-- Files and Folders Structure -->
<!-- Performance -->
<!-- Refactoring code -->
<!-- Tests -->
<!-- Not Sure? -->
## Proposal

View File

@@ -1,8 +0,0 @@
---
name: "🙋 Question"
about: "Further information is requested."
title: "[Question]"
labels: "question"
---
### Question

View File

@@ -1,7 +0,0 @@
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
# What changes this PR introduce?
## List any relevant issue numbers
## Is there anything you'd like reviewers to focus on?

View File

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

View File

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

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 }}

12
.gitignore vendored
View File

@@ -1,8 +1,10 @@
# dependencies
node_modules
.npm
.cache
package-lock.json
.pnpm-store
.apt
.pnp
.pnp.js
.yarn/install-state.gz
@@ -15,13 +17,16 @@ coverage
out/
dist/
build/
apps/website/public/curriculum-vitae/
.swc/
.react-email/
# misc
.DS_Store
*.pem
*.bak
.turbo
bin/
dump_*
# debug
npm-debug.log*
@@ -31,6 +36,7 @@ yarn-error.log*
# Storybook
*storybook.log
storybook-static
/public
# IDEs and editors
.idea
@@ -43,6 +49,7 @@ storybook-static
# local env files
.env
.env.dev
.env.production
.env*.local
@@ -51,4 +58,5 @@ storybook-static
# typescript
*.tsbuildinfo
# next-env.d.ts
next-env.d.ts
packages/i18n/src/translations/*.d.json.ts

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"],
}

16
.markdownlint-cli2.mjs Normal file
View File

@@ -0,0 +1,16 @@
import relativeLinksRule 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],
}
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,11 @@
{
"recommendations": [
"Vercel.turbo-vsc",
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"bradlc.vscode-tailwindcss",
"mikestead.dotenv",
"davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker"
"bradlc.vscode-tailwindcss",
"antfu.pnpm-catalog-lens",
"Lokalise.i18n-ally"
]
}

View File

@@ -20,9 +20,9 @@
"scope": "typescriptreact",
"prefix": "rfcs",
"body": [
"import type { Meta, StoryObj } from \"@storybook/react\"",
"import type { Meta, StoryObj } from \"@storybook/nextjs\"",
"",
"import { ${1:ComponentName} as ${1:ComponentName}Component } from \"./${1:ComponentName}\"",
"import { ${1:ComponentName} as ${1:ComponentName}Component } from \"./${1:ComponentName}.tsx\"",
"",
"const meta = {",
" title: \"${1:ComponentName}\",",

28
.vscode/settings.json vendored
View File

@@ -7,14 +7,26 @@
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
"source.organizeImports": "never"
},
"eslint.options": {
"ignorePath": ".gitignore"
"eslint.workingDirectories": [
{
"mode": "auto"
}
],
"files.associations": {
"*.css": "tailwindcss"
},
"prettier.ignorePath": ".gitignore",
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
"editor.quickSuggestions": {
"strings": "on"
},
"tailwindCSS.classFunctions": ["classNames", "cva"],
"tailwindCSS.experimental.configFile": "./configs/config-tailwind/styles.css",
"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

@@ -1,132 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<contact@theoludwig.fr>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][mozilla coc].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][faq]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[mozilla coc]: https://github.com/mozilla/diversity
[faq]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -1,10 +1,6 @@
# 💡 Contributing
# Contributing
Thanks a lot for your interest in contributing to **theoludwig.fr**! 🎉
## Code of Conduct
**theoludwig.fr** adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
Thanks a lot for your interest in contributing to **theoludwig.fr**!
## Types of contributions
@@ -31,8 +27,9 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
### Prerequisites
- [Node.js](https://nodejs.org/) >= 22.0.0
- [pnpm](https://pnpm.io/) >= 9.5.0
- [Node.js](https://nodejs.org/) >= v24.0.0 [(`nvm install 24`)](https://nvm.sh)
- [pnpm](https://pnpm.io/) v10.15.1 [(`npm install --global corepack@0.34.0 && corepack enable`)](https://github.com/nodejs/corepack)
- [Docker](https://www.docker.com/)
### Installation
@@ -40,6 +37,9 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
# Clone the repository
git clone git@github.com:theoludwig/theoludwig.git
# Go to the project root
cd theoludwig
# Configure environment variables
cp .env.example .env
cp apps/website/.env.example apps/website/.env
@@ -60,24 +60,28 @@ node --run dev
# Lint
node --run lint:editorconfig
node --run lint:markdown
node --run lint:prettier
node --run lint:eslint
node --run lint:typescript
node --run lint:eslint
node --run lint:prettier
# Tests
node --run test
# 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/)
```sh
# Setup and run all the services for you
docker compose up --build
VERSION=$(git describe --tags) docker compose up --build --detach
```
#### Services started
`theoludwig`: <http://127.0.0.1:3000>
`theoludwig`: <http://localhost:3000>

21
LICENSE
View File

@@ -1,21 +0,0 @@
# MIT License
Copyright (c) Théo LUDWIG <contact@theoludwig.fr>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,45 +1,63 @@
<h1 align="center"><a href="https://theoludwig.fr/">Théo LUDWIG</a></h1>
<p align="center">
<strong>Developer Full Stack • Open-Source Enthusiast</strong>
<strong>Developer Full Stack • Open-Source Enthusiast</strong>
</p>
<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://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://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.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="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="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://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="X/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.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="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>
<hr />
## 📜 About
## About
```json
{
"name": "Théo LUDWIG",
"pronouns": "He/Him",
"birthDate": "2003-03-31",
"nationality": "Alsace, France",
"interests": ["Developer Full Stack", "Open-Source Enthusiast"],
"skills": {
"programmingLanguages": ["JavaScript/TypeScript", "Python", "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"]
}
"name": "Théo LUDWIG",
"pronouns": "He/Him",
"birthDate": "2003-03-31",
"nationality": "Alsace, France",
"interests": ["Developer Full Stack", "Open-Source Enthusiast"],
"skills": {
"software-development": [
"TypeScript",
"React.js (+ Next.js)",
"Tailwind CSS",
"Node.js",
"tRPC/oRPC",
"PostgreSQL"
],
"sys-admin": [
"Docker",
"Proxmox",
"Caddy",
"GitHub Actions",
"GitLab CI/CD"
],
"software-tools": [
"GNU/Linux",
"Arch Linux",
"Visual Studio Code",
"Git"
],
"systems-programming": ["C/C++", "Rust", "Go"]
}
}
```
<hr />
## 📈 Statistics
## Statistics
<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/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?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" />
</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,15 +7,15 @@ const config: StorybookConfig = {
docs: {
defaultName: "Documentation",
},
stories: ["../../../packages/**/*.stories.tsx", "../stories/*.mdx"],
stories: [
"../../../packages/ui/src/**/*.stories.tsx",
"../../../packages/blog/src/**/*.stories.tsx",
"../stories/*.mdx",
],
addons: [
"@storybook/addon-essentials",
"@storybook/addon-storysource",
"@storybook/addon-a11y",
"@storybook/addon-links",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
"storybook-dark-mode",
"@storybook/addon-docs",
"@storybook/addon-a11y",
],
framework: {
name: "@storybook/nextjs",

View File

@@ -1,19 +1,30 @@
import "@repo/config-tailwind/styles.css"
import { defaultTranslationValues, Locale } from "@repo/i18n/config"
import i18nMessagesEnglish from "@repo/i18n/translations/en-US.json"
import type { Preview } from "@storybook/react"
import "./storybook-css-overrides.css"
import i18nMessages from "@repo/i18n/translations/en-US.json"
import { LOCALE_DEFAULT, TIMEZONE } from "@repo/utils/constants"
import type { Preview } from "@storybook/nextjs"
import { NextIntlClientProvider } from "next-intl"
import { ThemeProvider as NextThemeProvider } from "next-themes"
import React from "react"
const preview: Preview = {
initialGlobals: {
a11y: {
manual: true,
},
},
parameters: {
a11y: {
test: "error",
},
docs: {
codePanel: true,
},
nextjs: {
appDirectory: true,
},
options: {
storySort: {
order: ["Design System", "User Interface", "Errors", "Feature"],
order: ["Design System", "Layout", "Errors"],
},
},
backgrounds: { disable: true },
@@ -24,22 +35,21 @@ const preview: Preview = {
stylePreview: true,
},
controls: {
disableSaveFromUI: true,
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
date: /date$/i,
},
},
},
decorators: [
(Story) => {
const locale = "en-US" satisfies Locale
return (
<NextThemeProvider enableColorScheme={false}>
<NextIntlClientProvider
messages={i18nMessagesEnglish}
locale={locale}
defaultTranslationValues={defaultTranslationValues}
messages={i18nMessages}
locale={LOCALE_DEFAULT}
timeZone={TIMEZONE}
>
<Story />
</NextIntlClientProvider>

View File

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

View File

@@ -1,35 +0,0 @@
import type { TestRunnerConfig } from "@storybook/test-runner"
import { getStoryContext } from "@storybook/test-runner"
import { checkA11y, configureAxe, injectAxe } from "axe-playwright"
/*
* 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 = {
async preVisit(page) {
await injectAxe(page)
},
async postVisit(page, context) {
const storyContext = await getStoryContext(page, context)
if (storyContext.parameters?.a11y?.disable) {
return
}
await configureAxe(page, {
rules: storyContext.parameters?.a11y?.config?.rules,
})
await checkA11y(page, "#storybook-root", {
verbose: false,
detailedReport: true,
detailedReportOptions: {
html: true,
},
})
},
}
export default config

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

View File

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

View File

@@ -1,4 +1,9 @@
import { Meta, Title, ColorPalette, ColorItem } from "@storybook/blocks"
import {
Meta,
Title,
ColorPalette,
ColorItem,
} from "@storybook/addon-docs/blocks"
import tailwindConfig from "@repo/config-tailwind"
<Meta title="Design System/Colors" />
@@ -6,7 +11,7 @@ import tailwindConfig from "@repo/config-tailwind"
<Title>Colors</Title>
<ColorPalette>
{Object.entries(tailwindConfig.theme.colors).map(
{Object.entries(tailwindConfig.theme.extend.colors).map(
([colorName, colorValue]) => {
const colors = {}

View File

@@ -2,7 +2,6 @@ import sharedConfig from "@repo/config-tailwind"
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
const config = {
content: [".storybook/preview.tsx", "../../packages/**/*.tsx"],
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
PORT=3000
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.7.0-slim AS node-pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN npm install --global corepack@0.34.0 && corepack enable
ENV TURBO_TELEMETRY_DISABLED=1
ENV NEXT_TELEMETRY_DISABLED=1
ENV DO_NOT_TRACK=1
WORKDIR /usr/src/app
FROM node-pnpm AS builder
RUN pnpm install --global turbo@2.0.9
COPY ./ ./
RUN pnpm install --global turbo@2.5.6
RUN turbo prune @repo/website --docker
FROM node-pnpm AS installer
@@ -18,17 +21,20 @@ 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
COPY --from=builder /usr/src/app/out/full/ ./
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
FROM node-pnpm AS runner
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV NEXT_TELEMETRY_DISABLED=1
ENV IS_STANDALONE=true
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 applicationrunner
USER applicationrunner
COPY --from=installer /usr/src/app/apps/website/next.config.js ./
COPY --from=installer /usr/src/app/apps/website/next.config.ts ./
COPY --from=installer /usr/src/app/apps/website/package.json ./
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/.next/standalone ./
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/.next/static ./apps/website/.next/static

View File

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

View File

@@ -1,14 +1,15 @@
import { getBlogPosts } from "@repo/blog"
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 {
Section,
SectionDescription,
SectionTitle,
} from "@repo/ui/design/Section"
import { MainLayout } from "@repo/ui/MainLayout"
} from "@repo/ui/Layout/Section"
import { LOCALE_DEFAULT } from "@repo/utils/constants"
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 description =
@@ -35,8 +36,9 @@ interface BlogPageProps extends LocaleProps {}
const BlogPage: React.FC<BlogPageProps> = async (props) => {
const { params } = props
const { locale } = await params
// Enable static rendering
unstable_setRequestLocale(params.locale)
setRequestLocale(locale)
const posts = await getBlogPosts()

View File

@@ -0,0 +1,31 @@
import "@repo/config-tailwind/styles.css"
import type { Locale } from "@repo/utils/constants"
import { Footer } from "@repo/ui/Layout/Footer"
import { Header } from "@repo/ui/Layout/Header"
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
import { VERSION } from "@repo/utils/constants"
import { setRequestLocale } from "next-intl/server"
interface MainLayoutProps extends React.PropsWithChildren {
params: Promise<{
locale: string
}>
}
const MainLayout: React.FC<MainLayoutProps> = async (props) => {
const { children, params } = props
const { locale } = await params
// Enable static rendering
setRequestLocale(locale as Locale)
return (
<ThemeProvider>
<Header />
{children}
<Footer version={VERSION} />
</ThemeProvider>
)
}
export default MainLayout

View File

@@ -1,5 +1,5 @@
import { MainLayout } from "@repo/ui/MainLayout"
import { Spinner } from "@repo/ui/design/Spinner"
import { Spinner } from "@repo/ui/Design/Spinner"
import { MainLayout } from "@repo/ui/Layout/MainLayout"
const Loading: React.FC = () => {
return (

View File

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

View File

@@ -0,0 +1,24 @@
import "@repo/config-tailwind/styles.css"
import type { Locale } from "@repo/utils/constants"
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
import { setRequestLocale } from "next-intl/server"
interface CurriculumVitaeLayoutProps extends React.PropsWithChildren {
params: Promise<{
locale: string
}>
}
const CurriculumVitaeLayout: React.FC<CurriculumVitaeLayoutProps> = async (
props,
) => {
const { children, params } = props
const { locale } = await params
// Enable static rendering
setRequestLocale(locale as Locale)
return <ThemeProvider forcedTheme="light">{children}</ThemeProvider>
}
export default CurriculumVitaeLayout

View File

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

View File

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

View File

@@ -1,42 +0,0 @@
import type { LocaleProps } from "@repo/i18n/config"
import { About } from "@repo/ui/About"
import { RevealFade } from "@repo/ui/design/Section"
import { Interests } from "@repo/ui/Interests"
import { MainLayout } from "@repo/ui/MainLayout"
import { OpenSource } from "@repo/ui/OpenSource"
import { Portfolio } from "@repo/ui/Portfolio"
import { Skills } from "@repo/ui/Skills"
import { unstable_setRequestLocale } from "next-intl/server"
interface HomePageProps extends LocaleProps {}
const HomePage: React.FC<HomePageProps> = (props) => {
const { params } = props
// Enable static rendering
unstable_setRequestLocale(params.locale)
return (
<MainLayout>
<About />
<RevealFade>
<Interests />
</RevealFade>
<RevealFade>
<Skills />
</RevealFade>
<RevealFade>
<Portfolio />
</RevealFade>
<RevealFade>
<OpenSource />
</RevealFade>
</MainLayout>
)
}
export default HomePage

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,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,23 +1,26 @@
import { LOCALES, LOCALE_DEFAULT, LOCALE_PREFIX } from "@repo/i18n/config"
import createMiddleware from "next-intl/middleware"
import { routing } from "@repo/i18n/routing"
import createIntlMiddleware from "next-intl/middleware"
export default createMiddleware({
locales: LOCALES,
defaultLocale: LOCALE_DEFAULT,
localePrefix: LOCALE_PREFIX,
})
const intlMiddleware = createIntlMiddleware(routing)
export default intlMiddleware
export const config = {
matcher: [
// 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:
// https://github.com/vercel/next.js/issues/56398
"/(en-US|fr-FR)/:path*",
// Enable redirects that add missing locales
// (e.g. `/pathnames` -> `/en/pathnames`)
"/((?!_next|_vercel|.*\\..*).*)",
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
"/((?!api|_next/static|_next/image|images|favicon.ico|robots.txt).*)",
],
}

View File

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

View File

@@ -1,22 +0,0 @@
import createNextIntlPlugin from "next-intl/plugin"
const IS_STANDALONE = process.env.IS_STANDALONE === "true"
/** @type {import('next').NextConfig} */
const nextConfig = {
output: IS_STANDALONE ? "standalone" : undefined,
// https://github.com/hashicorp/next-mdx-remote/issues/436#issuecomment-2066971842
transpilePackages: ["next-mdx-remote", "shiki"],
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
}
const withNextIntl = createNextIntlPlugin()
export default withNextIntl(nextConfig)

View File

@@ -0,0 +1,37 @@
import { LOCALE_DEFAULT } from "@repo/utils/constants"
import type { NextConfig } from "next"
import createNextIntlPlugin from "next-intl/plugin"
import path from "node:path"
const IS_STANDALONE = process.env.IS_STANDALONE === "true"
const nextConfig: NextConfig = {
output: IS_STANDALONE ? "standalone" : undefined,
typedRoutes: true,
images: {
unoptimized: true,
},
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
}
const withNextIntl = createNextIntlPlugin({
experimental: {
createMessagesDeclaration: path.join(
process.cwd(),
"..",
"..",
"packages",
"i18n",
"src",
"translations",
`${LOCALE_DEFAULT}.json`,
),
},
})
export default withNextIntl(nextConfig)

View File

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

View File

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

View File

@@ -1 +0,0 @@
/*! modern-normalize v2.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */*,:before,:after{box-sizing:border-box}html{font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";line-height:1.15;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4}body{margin:0}hr{height:0;color:inherit}abbr[title]{text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}body{font-family:Montserrat,Arial,"sans-serif";background:#f0f0f0;color:#333;line-height:1.42857143;font-size:14px}hr{margin-top:15px;margin-bottom:15px;border:0;border-top:1px solid #eee}p{margin:0}strong{font-weight:600}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}.link-disguise,.link-disguise:hover{color:inherit}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.clear-margin{margin:0}.relative{position:relative}.center-block{display:block;margin-right:auto;margin-left:auto}.text-center{text-align:center}.text-muted{color:#777}.text-uppercase{text-transform:uppercase}.list-unstyled{padding-left:0;list-style:none}.main{padding:5px}.title{font-weight:600}.profile-card-wrapper{position:relative}.card-wrapper{float:none!important;padding:5px}.profile-card-wrapper .profile-card{padding:10px}.card{background:#fff;border-radius:3px;padding:10px 0}.profile-pic{padding:10px 0}.profile-pic img{width:100px;height:100px;border-radius:50%;vertical-align:middle;border:0}.contact-details{display:flex;justify-content:center}.contact-details .detail{position:relative;min-height:1px;padding:10px}.social-links{line-height:2.5}.experience-description{margin-top:10px}.background-details .detail{display:table}.background-details .detail .icon,.background-details .detail .info{display:table-cell}.background-details .detail .icon{color:#707070}.background-details .detail .icon{min-width:45px;max-width:45px;text-align:center}.icon img{width:20px;height:20px}.background-details .detail .mobile-title{display:none}.card-nested{min-height:0;border-width:1px 0 0 0}.card-skills{position:relative}.labels{line-height:2}.space-top{margin-top:10px}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:600;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label-keyword{display:inline-block;font-size:.9em;padding:5px;border:1px solid #357ebd;margin-right:5px}.label-keyword p{margin:0}.section-separated{display:flex}

View File

@@ -1 +0,0 @@
(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver(e=>{for(const t of e)if(t.type==="childList")for(const c of t.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&r(c)}).observe(document,{childList:!0,subtree:!0});function s(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?t.credentials="include":e.crossOrigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function r(e){if(e.ep)return;e.ep=!0;const t=s(e);fetch(e.href,t)}})();const i="31",l="03",u="2003",d=`${u}-${l}-${i}`,f=new Date(d),a=n=>{const o=new Date;let s=o.getFullYear()-n.getFullYear();const r=o.getMonth()-n.getMonth();return(r<0||r===0&&o.getDate()<n.getDate())&&s--,s},g=document.getElementById("year-old");g.textContent=a(f).toString();

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,85 @@
import nextPlugin from "@next/eslint-plugin-next"
import configConventions from "eslint-config-conventions"
import importXPlugin from "eslint-plugin-import-x"
import reactPlugin from "eslint-plugin-react"
import reactHooksPlugin from "eslint-plugin-react-hooks"
import typescriptESLint from "typescript-eslint"
export default typescriptESLint.config(
...configConventions,
reactHooksPlugin.configs.recommended,
reactPlugin.configs.flat.recommended,
{
ignores: [
".next",
"**/next.config.ts",
"**/eslint.config.js",
"**/tailwind.config.js",
"**/postcss.config.js",
"**/kysely.config.ts",
],
},
{
name: "config-eslint",
settings: {
react: {
version: "19.1.1",
},
},
plugins: {
"@next/next": nextPlugin,
"import-x": importXPlugin,
},
rules: {
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs["core-web-vitals"].rules,
"@next/next/no-html-link-for-pages": "off",
"@next/next/no-img-element": "off",
"react-hooks/react-compiler": "error",
"react/jsx-no-target-blank": "off",
"react/no-unknown-property": "off",
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react/self-closing-comp": [
"error",
{
component: true,
html: true,
},
],
"react/void-dom-elements-no-children": "error",
"react/jsx-boolean-value": "error",
"react/iframe-missing-sandbox": "off",
"no-restricted-imports": [
"error",
{
paths: [
{
name: "next/navigation",
importNames: [
"redirect",
"permanentRedirect",
"useRouter",
"usePathname",
],
message: "Please import from `@repo/i18n/routing` instead.",
},
],
},
],
"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,27 @@
{
"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"
}
},
"devDependencies": {
"typescript-eslint": "catalog:",
"eslint": "catalog:",
"eslint-config-conventions": "catalog:",
"eslint-plugin-promise": "catalog:",
"eslint-plugin-unicorn": "catalog:",
"eslint-plugin-import-x": "catalog:",
"@next/eslint-plugin-next": "catalog:",
"eslint-plugin-react": "catalog:",
"eslint-plugin-react-hooks": "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"
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,
},
},
})

5
configs/config-tailwind/index.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
import type { Config } from "tailwindcss"
declare const config: Config
export default config

View File

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

View File

@@ -0,0 +1,313 @@
@import "tailwindcss";
@config "./tailwind.config.js";
@source "../../apps/website/app";
@source "../../apps/website/components";
@source "../../apps/storybook/.storybook";
@source "../../packages/ui/src";
@source "../../packages/blog/src";
@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 {
[type="search"]::-webkit-search-decoration,
[type="search"]::-webkit-search-cancel-button {
appearance: none;
}
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
b,
strong {
@apply font-semibold;
}
i,
em {
@apply italic;
}
u {
@apply underline;
}
s {
@apply line-through;
}
abbr[title] {
@apply underline decoration-dotted underline-offset-2;
}
q,
blockquote {
@apply italic tracking-wider;
}
blockquote {
@apply border-gray-lighter border-l-4 pl-3 italic;
}
kbd {
@apply bg-gray-lighter rounded-md px-2 dark:text-black;
}
mark {
@apply bg-yellow rounded-md px-2;
}
ol {
@apply list-inside list-decimal;
}
ul {
@apply list-inside list-disc;
}
dfn {
@apply font-semibold italic;
cursor: help;
}
}
body {
@apply bg-background dark:bg-background-dark font-sans text-black dark:text-white;
}
@keyframes ripple {
to {
opacity: 0;
transform: scale(2);
}
}
.break-wrap-words {
word-wrap: break-word;
word-break: break-word;
}
.text-base {
@apply leading-8;
}
.prose {
@apply dark:text-gray-lighter !max-w-5xl scroll-smooth text-black;
}
.prose p {
@apply text-justify;
}
.prose ul,
.prose ol {
@apply list-outside;
}
.prose [id]::before {
content: "";
display: block;
height: 90px;
margin-top: -90px;
visibility: hidden;
}
.prose a {
@apply text-primary dark:text-primary-dark !font-semibold;
}
.prose strong {
@apply dark:text-gray-lighter text-black;
}
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
@apply mt-1;
}
.prose code {
color: #ce9178;
}
.prose :where(code):not(:where([class~="not-prose"] *))::before,
.prose :where(code):not(:where([class~="not-prose"] *))::after {
content: "";
}
.shiki {
white-space: pre-wrap !important;
}
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
code {
counter-reset: step;
counter-increment: step 0;
}
code .line::before {
content: counter(step);
counter-increment: step;
margin-right: 1rem;
text-align: right;
color: rgba(133, 133, 133, 0.8);
word-wrap: normal;
word-break: normal;
}
.katex .base {
display: inline !important;
white-space: normal !important;
width: 100% !important;
}
.curriculum-vitae {
color: #333;
font-family: Montserrat, sans-serif;
hr {
margin-top: 15px;
margin-bottom: 15px;
border: 0;
border-top: 1px solid #eee;
}
a {
color: #337ab7;
text-decoration: none;
}
a:focus,
a:hover {
color: #23527c;
text-decoration: underline;
}
.link-disguise {
color: inherit;
}
.link-disguise:hover {
color: inherit;
}
.h1,
.h2,
.h3 {
margin-top: 20px;
margin-bottom: 10px;
}
.h4,
.h5,
.h6 {
margin-top: 10px;
margin-bottom: 10px;
}
.h1,
.h2,
.h3,
.h4,
.h5,
.h6 {
font-family: inherit;
line-height: 1.1;
color: inherit;
}
.h3 {
font-size: 24px;
}
.h4 {
font-size: 18px;
}
.h5 {
font-size: 14px;
}
.text-muted {
color: #414141;
}
.list-unstyled {
padding-left: 0;
list-style: none;
}
.card-wrapper {
float: none !important;
padding: 5px;
}
.card {
background: white;
border-radius: 3px;
padding: 10px 0;
}
.profile-pic {
padding: 10px 0;
}
.profile-pic img {
width: 80px;
height: 80px;
border-radius: 50%;
vertical-align: middle;
border: 0;
}
.social-links {
line-height: 2.5;
}
.background-details .detail {
display: table;
}
.background-details .detail .icon,
.background-details .detail .info {
display: table-cell;
}
.background-details .detail .icon {
color: #707070;
}
.background-details .detail .icon {
min-width: 45px;
max-width: 45px;
text-align: center;
}
.icon img {
width: 20px;
height: 20px;
}
.background-details .detail .mobile-title {
display: none;
}
.card-nested {
min-height: 0;
}
.labels {
line-height: 2;
}
.label {
display: inline;
padding: 0.2em 0.6em 0.3em;
font-size: 75%;
font-weight: 600;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25em;
}
.label-keyword {
display: inline-block;
font-size: 0.9em;
padding: 5px;
border: 1px solid #357ebd;
margin-right: 5px;
}
.label-keyword p {
margin: 0;
}
}

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",
"version": "4.0.0-staging.1",
"version": "0.0.0-develop",
"private": true,
"files": [
"tsconfig.json"

View File

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

View File

@@ -1,39 +1,32 @@
{
"name": "repo",
"version": "4.0.0-staging.1",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903",
"packageManager": "pnpm@10.15.1+sha512.34e538c329b5553014ca8e8f4535997f96180a1d0f614339357449935350d924e22f8614682191264ec33d1462ac21561aff97f6bb18065351c162c7e8f6de67",
"engines": {
"node": ">=22.0.0",
"pnpm": ">=9.5.0"
"node": ">=24.0.0"
},
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"start": "turbo run start --parallel",
"build": "turbo run build",
"test": "turbo run test",
"lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint-cli2",
"lint:prettier": "prettier . --check",
"lint:eslint": "turbo run lint:eslint",
"lint:typescript": "turbo run lint:typescript",
"release": "semantic-release"
"lint:eslint": "turbo run lint:eslint",
"lint:prettier": "prettier . --check"
},
"devDependencies": {
"@saithodev/semantic-release-backmerge": "4.0.1",
"@semantic-release/exec": "6.0.3",
"@semantic-release/git": "10.0.1",
"editorconfig-checker": "5.1.8",
"editorconfig-checker": "catalog:",
"playwright": "catalog:",
"prettier": "3.3.3",
"prettier-plugin-tailwindcss": "0.6.5",
"markdownlint-cli2": "0.13.0",
"markdownlint": "0.34.0",
"markdownlint-rule-relative-links": "3.0.0",
"replace-in-files-cli": "3.0.0",
"semantic-release": "23.1.1",
"turbo": "2.0.9",
"prettier": "catalog:",
"prettier-plugin-tailwindcss": "catalog:",
"markdownlint-cli2": "catalog:",
"markdownlint": "catalog:",
"markdownlint-rule-relative-links": "catalog:",
"turbo": "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 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/blog",
"version": "4.0.0-staging.1",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"exports": {
@@ -9,7 +9,7 @@
"./BlogPostUI": "./src/BlogPostUI.tsx"
},
"scripts": {
"lint:eslint": "eslint src --max-warnings 0 --report-unused-disable-directives",
"lint:eslint": "eslint src --max-warnings 0",
"lint:typescript": "tsc --noEmit"
},
"dependencies": {
@@ -37,18 +37,19 @@
"react-icons": "catalog:"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"@storybook/blocks": "catalog:",
"@storybook/react": "catalog:",
"@storybook/test": "catalog:",
"storybook": "catalog:",
"@storybook/nextjs": "catalog:",
"eslint": "catalog:",
"postcss": "catalog:",
"tailwindcss": "catalog:",
"@tailwindcss/postcss": "catalog:",
"typescript-eslint": "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
interface Car {
carModel: string
carColor: "red" | "blue" | "yellow"
carModel: string
carColor: "red" | "blue" | "yellow"
}
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
interface Car {
model: string
color: "red" | "blue" | "yellow"
model: string
color: "red" | "blue" | "yellow"
}
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"
const createFile = async (
name: string,
isTemporary: boolean = false,
name: string,
isTemporary: boolean = false,
): Promise<void> => {
if (isTemporary) {
return await fs.promises.writeFile(path.join("temporary", name), "")
}
return await fs.promises.writeFile(name, "")
if (isTemporary) {
return await fs.promises.writeFile(path.join("temporary", name), "")
}
return await fs.promises.writeFile(name, "")
}
```
@@ -193,11 +193,11 @@ import fs from "node:fs"
import path from "node:path"
const createFile = async (name: string): Promise<void> => {
await fs.promises.writeFile(name, "")
await fs.promises.writeFile(name, "")
}
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
[user]
name = Username
email = email@example.com
name = Username
email = email@example.com
```
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
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
git remote add <remote> <url>
@@ -266,7 +268,7 @@ Sometimes, you want to compare what commits have been made between two branches,
```sh
[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:

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