1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2025-02-08 21:59:39 +01:00

chore: cleaner setup

This commit is contained in:
Théo LUDWIG 2025-02-08 20:00:47 +01:00
parent 270920111a
commit b63cc3a66e
Signed by: theoludwig
GPG Key ID: ADFE5A563D718F3B
69 changed files with 3393 additions and 5914 deletions

View File

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

View File

@ -10,15 +10,19 @@ jobs:
chromatic:
timeout-minutes: 30
runs-on: "ubuntu-latest"
env:
DO_NOT_TRACK: "1"
TURBO_TELEMETRY_DISABLED: "1"
NEXT_TELEMETRY_DISABLED: "1"
steps:
- uses: "actions/checkout@v4.2.2"
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.1.0"
uses: "actions/setup-node@v4.2.0"
with:
node-version: "22.x"
cache: "pnpm"

View File

@ -10,13 +10,17 @@ jobs:
ci:
timeout-minutes: 30
runs-on: "ubuntu-latest"
env:
DO_NOT_TRACK: "1"
TURBO_TELEMETRY_DISABLED: "1"
NEXT_TELEMETRY_DISABLED: "1"
steps:
- uses: "actions/checkout@v4.2.2"
- uses: "pnpm/action-setup@v4.0.0"
- uses: "pnpm/action-setup@v4.1.0"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.1.0"
uses: "actions/setup-node@v4.2.0"
with:
node-version: "22.x"
cache: "pnpm"

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.2.2"
with:
fetch-depth: 0
persist-credentials: false
- name: "Import GPG key"
uses: "crazy-max/ghaction-import-gpg@v6.1.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.1.0"
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"],
}

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

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

@ -15,7 +15,7 @@
],
"i18n-ally.localesPaths": ["./packages/i18n/src/translations/"],
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"i18n-ally.sortKeys": false,
"i18n-ally.sourceLanguage": "en-US",
"i18n-ally.displayLanguage": "en-US",
"i18n-ally.enabledFrameworks": ["next-intl", "general"],

View File

@ -31,8 +31,8 @@ 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.15.0 [(`corepack enable`)](https://nodejs.org/docs/latest-v22.x/api/corepack.html)
- [Node.js](https://nodejs.org/) >= 22.12.0 [(`nvm install 22`)](https://nvm.sh)
- [pnpm](https://pnpm.io/) >= 10.2.1 [(`corepack enable`)](https://nodejs.org/docs/latest-v22.x/api/corepack.html)
- [Docker](https://www.docker.com/)
### Installation
@ -83,9 +83,9 @@ node --run test
```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>

View File

@ -1,4 +1,5 @@
import "@repo/config-tailwind/styles.css"
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/react"
@ -7,6 +8,11 @@ import { ThemeProvider as NextThemeProvider } from "next-themes"
import React from "react"
const preview: Preview = {
globals: {
a11y: {
manual: true,
},
},
parameters: {
nextjs: {
appDirectory: true,

View File

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

View File

@ -1,5 +1,5 @@
import typescriptESLint from "typescript-eslint"
import config from "@repo/eslint-config"
import config from "@repo/config-eslint"
export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"],

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,14 +1,14 @@
{
"name": "@repo/storybook",
"version": "4.1.3",
"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 \"start\" http://127.0.0.1:6006 \"test:storybook\"",
"test:dev": "start-server-and-test \"dev\" http://127.0.0.1:6006 \"test:storybook\"",
"start": "node --experimental-strip-types 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"
},
@ -22,10 +22,11 @@
"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:",
@ -45,7 +46,6 @@
"axe-playwright": "catalog:",
"chromatic": "catalog:",
"eslint": "catalog:",
"http-server": "catalog:",
"start-server-and-test": "catalog:",
"storybook": "catalog:",
"storybook-dark-mode": "catalog:",

View File

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

View File

@ -1,14 +1,15 @@
FROM node:22.13.1-slim AS node-pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN npm install --global corepack@0.31.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
COPY ./ ./
RUN pnpm install --global turbo@2.3.3
RUN pnpm install --global turbo@2.4.0
RUN turbo prune @repo/website --docker
FROM node-pnpm AS installer
@ -20,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
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

View File

@ -1,5 +1,5 @@
import typescriptESLint from "typescript-eslint"
import configNextjs from "@repo/eslint-config/nextjs"
import configNextjs from "@repo/config-eslint/nextjs"
export default typescriptESLint.config(...configNextjs, {
files: ["**/*.ts", "**/*.tsx"],

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@repo/website",
"version": "4.1.3",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"imports": {
@ -28,7 +28,7 @@
"react-dom": "catalog:"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@types/node": "catalog:",
"@types/react": "catalog:",

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

@ -10,7 +10,6 @@ export default typescriptESLint.config(
"**/eslint.config.js",
"**/tailwind.config.js",
"**/postcss.config.js",
"**/vitest.config.ts",
"**/kysely.config.ts",
],
},

View File

@ -1,6 +1,6 @@
{
"name": "@repo/eslint-config",
"version": "4.1.3",
"name": "@repo/config-eslint",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"exports": {

View File

@ -1,5 +1,5 @@
import typescriptESLint from "typescript-eslint"
import config from "@repo/eslint-config"
import config from "@repo/config-eslint"
export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"],

View File

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

View File

@ -9,8 +9,9 @@
@tailwind utilities;
@layer base {
* {
min-width: 0;
[type="search"]::-webkit-search-decoration,
[type="search"]::-webkit-search-cancel-button {
appearance: none;
}
b,

View File

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

View File

@ -1,12 +1,12 @@
{
"name": "repo",
"version": "4.1.3",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c",
"packageManager": "pnpm@10.2.1+sha512.398035c7bd696d0ba0b10a688ed558285329d27ea994804a52bad9167d8e3a72bcb993f9699585d3ca25779ac64949ef422757a6c31102c12ab932e5cbe5cc92",
"engines": {
"node": ">=22.0.0",
"pnpm": ">=9.15.0"
"node": ">=22.12.0",
"pnpm": ">=10.2.1"
},
"scripts": {
"build": "turbo run build",
@ -17,13 +17,9 @@
"lint:markdown": "markdownlint-cli2",
"lint:typescript": "turbo run lint:typescript",
"lint:eslint": "turbo run lint:eslint",
"lint:prettier": "prettier . --check",
"release": "semantic-release"
"lint:prettier": "prettier . --check"
},
"devDependencies": {
"@saithodev/semantic-release-backmerge": "catalog:",
"@semantic-release/exec": "catalog:",
"@semantic-release/git": "catalog:",
"editorconfig-checker": "catalog:",
"playwright": "catalog:",
"prettier": "catalog:",
@ -31,8 +27,6 @@
"markdownlint-cli2": "catalog:",
"markdownlint": "catalog:",
"markdownlint-rule-relative-links": "catalog:",
"replace-in-files-cli": "catalog:",
"semantic-release": "catalog:",
"turbo": "catalog:",
"typescript": "catalog:"
}

View File

@ -1,5 +1,5 @@
import typescriptESLint from "typescript-eslint"
import configNextjs from "@repo/eslint-config/nextjs"
import configNextjs from "@repo/config-eslint/nextjs"
export default typescriptESLint.config(...configNextjs, {
files: ["**/*.ts", "**/*.tsx"],

View File

@ -1,6 +1,6 @@
{
"name": "@repo/blog",
"version": "4.1.3",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"exports": {
@ -37,7 +37,7 @@
"react-icons": "catalog:"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@types/node": "catalog:",
"@types/react": "catalog:",

View File

@ -1,5 +1,5 @@
import typescriptESLint from "typescript-eslint"
import config from "@repo/eslint-config"
import config from "@repo/config-eslint"
export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"],

View File

@ -1,6 +1,6 @@
{
"name": "@repo/i18n",
"version": "4.1.3",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"exports": {
@ -11,26 +11,23 @@
},
"scripts": {
"lint:eslint": "eslint src --max-warnings 0",
"lint:typescript": "tsc --noEmit",
"test": "vitest run"
"lint:typescript": "tsc --noEmit"
},
"dependencies": {
"@repo/utils": "workspace:*",
"deepmerge": "catalog:",
"next": "catalog:",
"next-intl": "catalog:",
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"eslint": "catalog:",
"typescript-eslint": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
"typescript": "catalog:"
}
}

View File

@ -1,6 +1,6 @@
import type en from "./translations/en-US.json"
import type fr from "./translations/fr-FR.json"
type Messages = typeof en
type Messages = typeof fr
declare global {
/**

View File

@ -1,9 +1,9 @@
import deepmerge from "deepmerge"
import type { AbstractIntlMessages } from "next-intl"
import { getRequestConfig } from "next-intl/server"
import type { Locale } from "@repo/utils/constants"
import { LOCALE_DEFAULT, LOCALES } from "@repo/utils/constants"
import { deepMerge } from "@repo/utils/objects"
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale
@ -15,7 +15,7 @@ export default getRequestConfig(async ({ requestLocale }) => {
const defaultMessages = (
await import(`./translations/${LOCALE_DEFAULT}.json`)
).default
const messages = deepmerge<AbstractIntlMessages>(
const messages = deepMerge<AbstractIntlMessages>(
defaultMessages,
userMessages,
)

View File

@ -4,6 +4,13 @@ import { LOCALES, LOCALE_DEFAULT, LOCALE_PREFIX } from "@repo/utils/constants"
import { defineRouting } from "next-intl/routing"
import type { Locale } from "@repo/utils/constants"
// Countries: https://github.com/umpirsky/country-list/blob/master/data/en/country.json
// Country flag picture: https://purecatamphetamine.github.io/country-flag-icons/3x2/US.svg
// Locale codes: https://simplelocalize.io/data/locales/
// Locale code is a combination of ISO 639-1 language code and ISO 3166-1 country code.
// For example, `fr-FR` is a locale code for French language in France.
export interface LocaleProps {
params: Promise<{
locale: Locale

View File

@ -1,7 +0,0 @@
import { expectTypeOf, test } from "vitest"
import en from "../translations/en-US.json"
import fr from "../translations/fr-FR.json"
test("translations types should match", () => {
expectTypeOf(en).toEqualTypeOf(fr)
})

View File

@ -1,4 +1,24 @@
{
"meta": {
"description": "Developer Full Stack • Open-Source Enthusiast",
"title": "Théo LUDWIG"
},
"locales": {
"en-US": "English",
"fr-FR": "French"
},
"loading": "Loading...",
"errors": {
"error": "Error",
"not-found": "Not Found",
"page-doesnt-exist": "This page doesn't exist!",
"return-to-home-page": "Return to the home page?",
"server-error": "Internal Server Error!",
"try-again": "Try again?"
},
"footer": {
"all-rights-reserved": "All rights reserved"
},
"curriculum-vitae": {
"about": {
"description": "I constantly wonder how to improve our present, to make our future better, particularly thanks to the advancements in computer science. <br></br> My priority is to craft intuitive user experiences (UX), that meet the needs of the users in the most efficient way possible.",
@ -84,17 +104,6 @@
"title": "Work experiences"
}
},
"errors": {
"error": "Error",
"not-found": "Not Found",
"page-doesnt-exist": "This page doesn't exist!",
"return-to-home-page": "Return to the home page?",
"server-error": "Internal Server Error!",
"try-again": "Try again?"
},
"footer": {
"all-rights-reserved": "All rights reserved"
},
"home": {
"about": {
"birth-date": {
@ -150,13 +159,5 @@
"software-tools": "Software and tools",
"title": "Skills"
}
},
"locales": {
"en-US": "English",
"fr-FR": "French"
},
"meta": {
"description": "Developer Full Stack • Open-Source Enthusiast",
"title": "Théo LUDWIG"
}
}

View File

@ -1,4 +1,24 @@
{
"meta": {
"description": "Développeur Full Stack • Enthousiaste de l'Open-Source",
"title": "Théo LUDWIG"
},
"locales": {
"en-US": "Anglais",
"fr-FR": "Français"
},
"loading": "Chargement...",
"errors": {
"error": "Erreur",
"not-found": "Introuvable",
"page-doesnt-exist": "Cette page n'existe pas !",
"return-to-home-page": "Retour à la page d'accueil ?",
"server-error": "Erreur interne du serveur !",
"try-again": "Réessayer ?"
},
"footer": {
"all-rights-reserved": "Tous droits réservés"
},
"curriculum-vitae": {
"about": {
"description": "Je me demande constamment comment améliorer notre présent, afin de rendre notre futur meilleur, particulièrement grâce aux progrès de l'informatique. <br></br> Ma priorité réside dans la création d'expériences utilisateurs (UX) intuitives, répondant aux besoins des utilisateurs de la manière la plus efficace que possible.",
@ -84,17 +104,6 @@
"title": "Expériences professionnelles"
}
},
"errors": {
"error": "Erreur",
"not-found": "Introuvable",
"page-doesnt-exist": "Cette page n'existe pas !",
"return-to-home-page": "Retour à la page d'accueil ?",
"server-error": "Erreur interne du serveur !",
"try-again": "Réessayer ?"
},
"footer": {
"all-rights-reserved": "Tous droits réservés"
},
"home": {
"about": {
"birth-date": {
@ -150,13 +159,5 @@
"software-tools": "Logiciels et outils",
"title": "Compétences"
}
},
"locales": {
"en-US": "Anglais",
"fr-FR": "Français"
},
"meta": {
"description": "Développeur Full Stack • Enthousiaste de l'Open-Source",
"title": "Théo LUDWIG"
}
}

View File

@ -1,9 +0,0 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
typecheck: {
enabled: true,
},
},
})

View File

@ -1,5 +1,5 @@
import typescriptESLint from "typescript-eslint"
import configNextjs from "@repo/eslint-config/nextjs"
import configNextjs from "@repo/config-eslint/nextjs"
export default typescriptESLint.config(...configNextjs, {
files: ["**/*.ts", "**/*.tsx"],

View File

@ -1,6 +1,6 @@
{
"name": "@repo/react-hooks",
"version": "4.1.3",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"exports": {
@ -9,28 +9,21 @@
},
"scripts": {
"lint:eslint": "eslint src --max-warnings 0",
"lint:typescript": "tsc --noEmit",
"test": "vitest run --browser.headless",
"test:ui": "vitest --ui --no-open"
"lint:typescript": "tsc --noEmit"
},
"dependencies": {
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@testing-library/react": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"@vitest/browser": "catalog:",
"@vitest/coverage-v8": "catalog:",
"@vitest/ui": "catalog:",
"eslint": "catalog:",
"playwright": "catalog:",
"typescript-eslint": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
"typescript-eslint": "catalog:"
}
}

View File

@ -1,83 +0,0 @@
import { act, renderHook } from "@testing-library/react"
import { describe, expect, it } from "vitest"
import { useBoolean } from "../useBoolean.ts"
describe("useBoolean", () => {
const initialValues = [true, false]
for (const initialValue of initialValues) {
it(`should set the initial value to ${initialValue}`, () => {
// Arrange - Given
const { result } = renderHook(() => {
return useBoolean({ initialValue })
})
// Assert - Then
expect(result.current.value).toBe(initialValue)
})
}
it("should by default set the initial value to false", () => {
// Arrange - Given
const { result } = renderHook(() => {
return useBoolean()
})
// Assert - Then
expect(result.current.value).toBe(false)
})
it("should toggle the value", () => {
// Arrange - Given
const { result } = renderHook(() => {
return useBoolean({ initialValue: false })
})
// Act - When
act(() => {
return result.current.toggle()
})
// Assert - Then
expect(result.current.value).toBe(true)
// Act - When
act(() => {
return result.current.toggle()
})
// Assert - Then
expect(result.current.value).toBe(false)
})
it("should set the value to true", () => {
// Arrange - Given
const { result } = renderHook(() => {
return useBoolean({ initialValue: false })
})
// Act - When
act(() => {
return result.current.setTrue()
})
// Assert - Then
expect(result.current.value).toBe(true)
})
it("should set the value to false", () => {
// Arrange - Given
const { result } = renderHook(() => {
return useBoolean({ initialValue: true })
})
// Act - When
act(() => {
return result.current.setFalse()
})
// Assert - Then
expect(result.current.value).toBe(false)
})
})

View File

@ -1,16 +0,0 @@
import { renderHook } from "@testing-library/react"
import { describe, expect, it } from "vitest"
import { useIsMounted } from "../useIsMounted.ts"
describe("useIsMounted", () => {
it("should return true", () => {
// Arrange - Given
const { result } = renderHook(() => {
return useIsMounted()
})
// Assert - Then
expect(result.current.isMounted).toBe(true)
})
})

View File

@ -18,11 +18,11 @@ export interface UseBooleanInput {
/**
* Hook to manage a boolean state.
* @param options
* @param input
* @returns
*/
export const useBoolean = (options: UseBooleanInput = {}): UseBooleanOutput => {
const { initialValue = false } = options
export const useBoolean = (input: UseBooleanInput = {}): UseBooleanOutput => {
const { initialValue = false } = input
const [value, setValue] = useState(initialValue)

View File

@ -1,19 +0,0 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
optimizeDeps: {
include: ["@vitest/coverage-v8/browser"],
},
test: {
browser: {
provider: "playwright",
enabled: true,
name: "chromium",
screenshotFailures: false,
},
coverage: {
enabled: true,
provider: "v8",
},
},
})

View File

@ -1,5 +1,5 @@
import typescriptESLint from "typescript-eslint"
import configNextjs from "@repo/eslint-config/nextjs"
import configNextjs from "@repo/config-eslint/nextjs"
export default typescriptESLint.config(...configNextjs, {
files: ["**/*.ts", "**/*.tsx"],

View File

@ -1,6 +1,6 @@
{
"name": "@repo/ui",
"version": "4.1.3",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"exports": {
@ -40,7 +40,7 @@
"react-icons": "catalog:"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",

View File

@ -1,5 +1,5 @@
import typescriptESLint from "typescript-eslint"
import config from "@repo/eslint-config"
import config from "@repo/config-eslint"
export default typescriptESLint.config(...config, {
files: ["**/*.ts", "**/*.tsx"],

View File

@ -1,29 +1,28 @@
{
"name": "@repo/utils",
"version": "4.1.3",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"exports": {
"./constants": "./src/constants.ts",
"./dates": "./src/dates.ts",
"./strings": "./src/strings.ts"
"./objects": "./src/objects.ts",
"./strings": "./src/strings.ts",
"./types": "./src/types.ts",
"./urls": "./src/urls.ts"
},
"scripts": {
"lint:eslint": "eslint src --max-warnings 0",
"lint:typescript": "tsc --noEmit",
"test": "vitest run",
"test:ui": "vitest --ui --no-open"
"test": "node --experimental-strip-types --test"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/config-eslint": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@types/node": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"@vitest/coverage-v8": "catalog:",
"@vitest/ui": "catalog:",
"eslint": "catalog:",
"typescript-eslint": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
"typescript": "catalog:"
}
}

View File

@ -13,7 +13,7 @@ export const THEMES = ["light", "dark"] as const
export type Theme = (typeof THEMES)[number]
export const THEME_DEFAULT = "light" satisfies Theme
export const TIMEZONE = process.env["TZ"] ?? "UTC"
export const TIMEZONE = process.env["TZ"] ?? "Europe/Paris"
export const BIRTH_DATE_DAY = "31"
export const BIRTH_DATE_MONTH = "03"

View File

@ -0,0 +1,19 @@
export const deepMerge = <
Object1 extends object,
Object2 extends object = Object1,
>(
object1: Object1,
object2: Object2,
): Object1 & Object2 => {
const result = { ...object1 } as Object1 & Object2
for (const key in object2) {
if (Object.hasOwn(object2, key)) {
if (typeof object2[key] === "object" && object2[key] !== null) {
result[key] = deepMerge(result[key] as any, object2[key] as any)
} else {
result[key] = object2[key] as any
}
}
}
return result
}

View File

@ -1,36 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest"
describe("VERSION", () => {
afterEach(() => {
vi.unstubAllEnvs()
vi.resetModules()
vi.restoreAllMocks()
})
it('should return "0.0.0-development" when NODE_ENV is development', async () => {
// Arrange - Given
vi.stubEnv("NODE_ENV", "development")
// Act - When
const { VERSION } = await import("../constants.ts")
// Assert - Then
const expected = "0.0.0-development"
expect(VERSION).toEqual(expected)
})
it("should return the version from package.json when NODE_ENV is not development", async () => {
// Arrange - Given
vi.stubEnv("NODE_ENV", "production")
vi.mock("../../package.json", () => {
return { default: { version: "1.0.0" } }
})
// Act - When
const { VERSION } = await import("../constants.ts")
// Assert - Then
const expected = "1.0.0"
expect(VERSION).toEqual(expected)
})
})

View File

@ -1,79 +1,19 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import assert from "node:assert/strict"
import { describe, it } from "node:test"
import { getISODate } from "../dates.ts"
import { getAge, getISODate } from "../dates.ts"
describe("dates", () => {
describe("getISODate", () => {
it("should return the correct date in ISO format (e.g: 2012-05-23)", () => {
// Arrange - Given
const input = new Date("2012-05-23")
describe("getISODate", () => {
it("should return the correct date in ISO format (e.g: 2012-05-23)", () => {
// Arrange - Given
const input = new Date("2012-05-23")
// Act - When
const output = getISODate(input)
// Act - When
const output = getISODate(input)
// Assert - Then
const expected = "2012-05-23"
expect(output).toEqual(expected)
})
})
describe("getAge", () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it("should return the correct age based on the birth date", () => {
// Arrange - Given
vi.setSystemTime(new Date("2018-03-20"))
const birthDate = new Date("1980-02-20")
// Act - When
const output = getAge(birthDate)
// Assert - Then
const expected = 38
expect(output).toEqual(expected)
})
it("should return the correct age based on the birth date when the birthday has not happened yet", () => {
// Arrange - Given
vi.setSystemTime(new Date("2018-03-20"))
const birthDate = new Date("1980-07-20")
// Act - When
const output = getAge(birthDate)
// Assert - Then
const expected = 37
expect(output).toEqual(expected)
})
it("should return the correct age based on the birth date when the birthday is today", () => {
// Arrange - Given
vi.setSystemTime(new Date("2018-03-20"))
const birthDate = new Date("1980-03-20")
// Act - When
const output = getAge(birthDate)
// Assert - Then
const expected = 38
expect(output).toEqual(expected)
})
it("should return the correct age based on the birth date when the birthday has not happened yet, but will happen this month", () => {
// Arrange - Given
vi.setSystemTime(new Date("2018-03-20"))
const birthDate = new Date("1980-03-25")
// Act - When
const output = getAge(birthDate)
// Assert - Then
const expected = 37
expect(output).toEqual(expected)
// Assert - Then
const expected = "2012-05-23"
assert.strictEqual(output, expected)
})
})
})

View File

@ -0,0 +1,85 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"
import { deepMerge } from "../objects.ts"
describe("objects", () => {
describe("deepMerge", () => {
it("should merge two simple objects", () => {
// Arrange - Given
const object1 = { a: 1, b: 2 }
const object2 = { b: 3, c: 4 }
// Act - When
const output = deepMerge(object1, object2)
// Assert - Then
const expected = { a: 1, b: 3, c: 4 }
assert.deepStrictEqual(output, expected)
})
it("should deeply merge nested objects", () => {
// Arrange - Given
const object1 = { a: 1, b: { x: 2, y: 3 } }
const object2 = { b: { y: 4, z: 5 }, c: 6 }
// Act - When
const output = deepMerge(object1, object2)
// Assert - Then
const expected = { a: 1, b: { x: 2, y: 4, z: 5 }, c: 6 }
assert.deepStrictEqual(output, expected)
})
it("should overwrite primitive values", () => {
// Arrange - Given
const object1 = { a: 1, b: "hello" }
const object2 = { a: 2, b: "world" }
// Act - When
const output = deepMerge(object1, object2)
// Assert - Then
const expected = { a: 2, b: "world" }
assert.deepStrictEqual(output, expected)
})
it("should return the second object if the first is empty", () => {
// Arrange - Given
const object1 = {}
const object2 = { a: 1, b: 2 }
// Act - When
const output = deepMerge(object1, object2)
// Assert - Then
const expected = { a: 1, b: 2 }
assert.deepStrictEqual(output, expected)
})
it("should return the first object if the second is empty", () => {
// Arrange - Given
const object1 = { a: 1, b: 2 }
const object2 = {}
// Act - When
const output = deepMerge(object1, object2)
// Assert - Then
const expected = { a: 1, b: 2 }
assert.deepStrictEqual(output, expected)
})
it("should handle null and undefined values correctly", () => {
// Arrange - Given
const object1 = { a: 1, b: null }
const object2 = { b: { c: 2 }, d: undefined }
// Act - When
const output = deepMerge(object1, object2)
// Assert - Then
const expected = { a: 1, b: { c: 2 }, d: undefined }
assert.deepStrictEqual(output, expected)
})
})
})

View File

@ -1,41 +1,43 @@
import { describe, expect, it } from "vitest"
import assert from "node:assert/strict"
import { describe, it } from "node:test"
import { capitalize } from "../strings.ts"
describe("capitalize", () => {
it("should capitalize the first letter of a string", () => {
// Arrange - Given
const input = "hello, world!"
describe("strings", () => {
describe("capitalize", () => {
it("should capitalize the first letter of a string", () => {
// Arrange - Given
const input = "hello, world!"
// Act - When
const output = capitalize(input)
// Act - When
const output = capitalize(input)
// Assert - Then
const expected = "Hello, world!"
expect(output).toEqual(expected)
})
// Assert - Then
const expected = "Hello, world!"
assert.strictEqual(output, expected)
})
it("should return an empty string when the input is an empty string", () => {
// Arrange - Given
const input = ""
it("should return an empty string when the input is an empty string", () => {
// Arrange - Given
const input = ""
// Act - When
const output = capitalize(input)
// Act - When
const output = capitalize(input)
// Assert - Then
const expected = ""
expect(output).toEqual(expected)
})
// Assert - Then
const expected = ""
assert.strictEqual(output, expected)
})
it("should return the same string when the first letter is already capitalized", () => {
// Arrange - Given
const input = "Hello, world!"
it("should return the same string when the first letter is already capitalized", () => {
// Arrange - Given
const input = "Hello, world!"
// Act - When
const output = capitalize(input)
// Act - When
const output = capitalize(input)
// Assert - Then
const expected = "Hello, world!"
expect(output).toEqual(expected)
// Assert - Then
const expected = "Hello, world!"
assert.strictEqual(output, expected)
})
})
})

View File

@ -0,0 +1,80 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"
import { LOCALE_DEFAULT } from "../constants.ts"
import { getPathnameWithoutLocale } from "../urls.ts"
describe("urls", () => {
describe("getPathnameWithoutLocale", () => {
it("should return the pathname without the known locale prefix", () => {
// Arrange - Given
const input = `/${LOCALE_DEFAULT}/about`
// Act - When
const output = getPathnameWithoutLocale(input)
// Assert - Then
const expected = "/about"
assert.strictEqual(output, expected)
})
it("should return the same pathname when the input does not start with a known locale prefix", () => {
// Arrange - Given
const input = "/about"
// Act - When
const output = getPathnameWithoutLocale(input)
// Assert - Then
const expected = "/about"
assert.strictEqual(output, expected)
})
it("should return the same pathname when the input starts with an unknown locale prefix", () => {
// Arrange - Given
const input = "/abc-ABC/about"
// Act - When
const output = getPathnameWithoutLocale(input)
// Assert - Then
const expected = "/abc-ABC/about"
assert.strictEqual(output, expected)
})
it("should return the index route when the input is an empty string", () => {
// Arrange - Given
const input = ""
// Act - When
const output = getPathnameWithoutLocale(input)
// Assert - Then
const expected = "/"
assert.strictEqual(output, expected)
})
it("should return the index route when the input starts with a known locale prefix and with a trailing slash", () => {
// Arrange - Given
const input = `/${LOCALE_DEFAULT}/`
// Act - When
const output = getPathnameWithoutLocale(input)
// Assert - Then
const expected = "/"
assert.strictEqual(output, expected)
})
it("should return the index route when the input starts with a known locale prefix and without a trailing slash", () => {
// Arrange - Given
const input = `/${LOCALE_DEFAULT}`
// Act - When
const output = getPathnameWithoutLocale(input)
// Assert - Then
const expected = "/"
assert.strictEqual(output, expected)
})
})
})

View File

@ -0,0 +1,49 @@
/**
* Matches any [primitive value](https://developer.mozilla.org/en-US/docs/Glossary/Primitive).
*/
export type Primitive =
| null
| undefined
| string
| number
| boolean
| symbol
| bigint
export type Satisfies<U, T extends U> = T
export type OmitStrict<T, K extends keyof T> = Omit<T, K>
export type PickStrict<T, K extends keyof T> = Pick<T, K>
export type OverrideStrict<
Type,
NewType extends {
[Key in keyof Type]?: unknown
},
> = Omit<Type, keyof NewType> & NewType
export type PartialDeep<T> = T extends object
? {
[P in keyof T]?: PartialDeep<T[P]>
}
: T
export type Status = "error" | "idle" | "pending" | "success"
/**
* Allows creating a union type by combining primitive types and literal types without sacrificing auto-completion in IDEs for the literal type part of the union.
*
* @see https://github.com/Microsoft/TypeScript/issues/29729
*
* @example
```
// Before
type Pet = 'dog' | 'cat' | string;
// After
type Pet2 = LiteralUnion<'dog' | 'cat', string>;
```
*/
export type LiteralUnion<LiteralType, BaseType extends Primitive> =
| LiteralType
| (BaseType & Record<never, never>)

View File

@ -0,0 +1,18 @@
import { LOCALES } from "./constants.ts"
/**
* Get the pathname without the known locale prefix.
* @param input
* @returns
* @example getRoutePathnameWithoutLocale("/fr-FR/about") // "/about"
*/
export const getPathnameWithoutLocale = (input: string): string => {
const locale = LOCALES.find((locale) => {
return input.startsWith(`/${locale}`)
})
const pathname = locale != null ? input.slice(locale.length + 1) : input
if (pathname.length <= 0) {
return `/${pathname}`
}
return pathname
}

View File

@ -1,10 +0,0 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
coverage: {
enabled: true,
provider: "v8",
},
},
})

8157
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,30 @@
packages:
- "apps/*"
- "configs/*"
- "packages/*"
catalog:
# Turborepo and Releases
"turbo": "2.3.3"
"@saithodev/semantic-release-backmerge": "4.0.1"
"@semantic-release/exec": "6.0.3"
"@semantic-release/git": "10.0.1"
"semantic-release": "23.1.1"
"replace-in-files-cli": "3.0.0"
# Turborepo
"turbo": "2.4.0"
# Utils
"deepmerge": "4.3.1"
"mime": "4.0.6"
# React.js/Next.js
"next": "15.0.4"
"next-intl": "3.26.0"
"next": &next "15.1.6"
"next-intl": "3.26.3"
"next-themes": "0.4.4"
"nuqs": "2.3.2"
"react": "19.0.0"
"react-dom": "19.0.0"
"react-icons": "5.4.0"
"@types/react": "19.0.1"
"@types/react-dom": "19.0.1"
"@types/react": "19.0.8"
"@types/react-dom": "19.0.3"
# Blog
"@giscus/react": "3.0.0"
"@giscus/react": "3.1.0"
"gray-matter": "4.0.3"
"katex": "0.16.11"
"katex": "0.16.21"
"next-mdx-remote": "5.0.0"
"@mdx-js/mdx": "3.1.0"
"rehype-katex": "7.0.1"
@ -39,66 +36,60 @@ catalog:
"@shikijs/rehype": "1.24.0"
# Markdown Lint
"markdownlint-cli2": "0.15.0"
"markdownlint": "0.36.1"
"markdownlint-rule-relative-links": "3.0.0"
"markdownlint-cli2": "0.17.2"
"markdownlint": "0.37.4"
"markdownlint-rule-relative-links": "4.0.1"
# TypeScript
"typescript": "5.7.2"
"typescript": "5.7.3"
"@total-typescript/ts-reset": "0.6.1"
"@types/node": "22.10.1"
"@types/node": "22.13.1"
# ESLint
"globals": "15.13.0"
"typescript-eslint": "8.17.0"
"globals": "15.14.0"
"typescript-eslint": "8.23.0"
"@eslint/eslintrc": "3.2.0"
"eslint": "9.16.0"
"eslint-config-conventions": "17.0.1"
"eslint": "9.20.0"
"eslint-config-conventions": "18.0.2"
"eslint-plugin-promise": "7.2.1"
"eslint-plugin-unicorn": "56.0.1"
"eslint-config-next": "15.0.4"
"eslint-plugin-storybook": "0.11.1"
"eslint-plugin-tailwindcss": "3.17.5"
"eslint-plugin-import-x": "4.5.0"
"eslint-config-next": *next
"eslint-plugin-storybook": "0.11.2"
"eslint-plugin-tailwindcss": "3.18.0"
"eslint-plugin-import-x": "4.6.1"
# Prettier
"prettier": "3.4.2"
"prettier-plugin-tailwindcss": "0.6.9"
"editorconfig-checker": "6.0.0"
"prettier-plugin-tailwindcss": "0.6.11"
"editorconfig-checker": "6.0.1"
# Storybook
"@chromatic-com/storybook": "3.2.2"
"@storybook/addon-a11y": "8.4.7"
"@storybook/addon-essentials": "8.4.7"
"@storybook/addon-interactions": "8.4.7"
"@storybook/addon-storysource": "8.4.7"
"@storybook/addon-themes": "8.4.7"
"@storybook/blocks": "8.4.7"
"@storybook/nextjs": "8.4.7"
"@storybook/react": "8.4.7"
"@storybook/test": "8.4.7"
"@storybook/test-runner": "0.20.1"
"chromatic": "11.20.0"
"http-server": "14.1.1"
"storybook": "8.4.7"
"storybook": &storybook "8.5.3"
"@storybook/addon-essentials": *storybook
"@storybook/addon-storysource": *storybook
"@storybook/addon-a11y": *storybook
"@storybook/addon-interactions": *storybook
"@storybook/blocks": *storybook
"@storybook/nextjs": *storybook
"@storybook/react": *storybook
"@storybook/test": *storybook
"@storybook/addon-themes": *storybook
"@storybook/test-runner": "0.21.0"
"@chromatic-com/storybook": "3.2.4"
"chromatic": "11.25.2"
"storybook-dark-mode": "4.0.2"
# Testing
"playwright": "1.49.0"
"@playwright/test": "1.49.0"
"axe-playwright": "2.0.3"
"start-server-and-test": "2.0.8"
"@vitest/browser": "2.1.8"
"@vitest/coverage-v8": "2.1.8"
"@vitest/ui": "2.1.8"
"vitest": "2.1.8"
"@testing-library/react": "16.1.0"
"playwright": &playwright "1.50.1"
"@playwright/test": *playwright
"axe-playwright": "2.1.0"
"start-server-and-test": "2.0.10"
# CSS
"postcss": "8.4.49"
"tailwindcss": "3.4.16"
"postcss": "8.5.1"
"tailwindcss": "3.4.17"
"@tailwindcss/typography": "0.5.15"
"tailwind-merge": "2.5.5"
"tailwind-merge": "2.6.0"
"clsx": "2.1.1"
"cva": "1.0.0-beta.2"
"@fontsource/montserrat": "5.1.0"
"cva": "1.0.0-beta.3"
"@fontsource/montserrat": "5.1.1"

View File

@ -1,5 +1,5 @@
{
"$schema": "https://turbo.build/schema.json",
"$schema": "./node_modules/turbo/schema.json",
"ui": "tui",
"tasks": {
"build": {
@ -15,7 +15,7 @@
},
"test": {
"dependsOn": ["^test"],
"outputs": ["coverage/**"]
"outputs": []
},
"lint:eslint-transit-node": {
"dependsOn": ["^lint:eslint-transit-node"]