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

Compare commits

..

12 Commits

100 changed files with 5247 additions and 7434 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,2 +1,2 @@
TZ=UTC
TZ=Europe/Paris
WEBSITE_PORT=3000

View File

@ -6,8 +6,8 @@ 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.
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

View File

@ -10,17 +10,21 @@ 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"
node-version: "24.x"
cache: "pnpm"
- name: "Install dependencies"

View File

@ -10,15 +10,19 @@ 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"
node-version: "24.x"
cache: "pnpm"
- name: "Install dependencies"

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

19
.markdownlint-cli2.mjs Normal file
View File

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

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

@ -6,7 +6,6 @@
"davidanson.vscode-markdownlint",
"bradlc.vscode-tailwindcss",
"mikestead.dotenv",
"ms-azuretools.vscode-docker",
"antfu.pnpm-catalog-lens",
"Lokalise.i18n-ally"
]

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

@ -20,21 +20,16 @@ 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
- 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
- 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
- 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

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.14.4 [(`corepack enable`)](https://nodejs.org/docs/latest-v22.x/api/corepack.html)
- [Node.js](https://nodejs.org/) >= v24.0.0 [(`nvm install 24`)](https://nvm.sh)
- [pnpm](https://pnpm.io/) v10.10.0 [(`npm install --global corepack@0.32.0 && corepack enable`)](https://github.com/nodejs/corepack)
- [Docker](https://www.docker.com/)
### Installation
@ -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,18 +1,18 @@
<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="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 />
@ -21,17 +21,28 @@
```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": {
"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"
]
}
}
```
@ -40,6 +51,6 @@
## 📈 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

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

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.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 \"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,12 +46,12 @@
"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

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

View File

@ -3,5 +3,5 @@
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ESNext"]
},
"include": ["./.storybook/**/*.ts", "./.storybook/**/*.tsx"]
"include": ["http-server.ts", "./.storybook/**/*.ts", "./.storybook/**/*.tsx"]
}

View File

@ -1,5 +1,5 @@
{
"$schema": "https://turbo.build/schema.json",
"$schema": "../../node_modules/turbo/schema.json",
"extends": ["//"],
"tasks": {
"test": {

View File

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

View File

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

View File

@ -1,14 +1,15 @@
FROM node:22.12.0-slim AS node-pnpm
FROM node:24.0.0-slim AS node-pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
RUN npm install --global corepack@0.32.0 && corepack enable
ENV TURBO_TELEMETRY_DISABLED=1
ENV NEXT_TELEMETRY_DISABLED=1
ENV DO_NOT_TRACK=1
WORKDIR /usr/src/app
FROM node-pnpm AS builder
COPY ./ ./
RUN pnpm install --global turbo@2.3.3
RUN pnpm install --global turbo@2.5.3
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

@ -2,13 +2,20 @@ import "@repo/config-tailwind/styles.css"
import type { LocaleProps } from "@repo/i18n/routing"
import type { Locale } from "@repo/utils/constants"
import { LOCALES } from "@repo/utils/constants"
import type { Metadata } from "next"
import type { Metadata, Viewport } from "next"
import { NextIntlClientProvider } from "next-intl"
import {
getMessages,
getTranslations,
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,
@ -18,7 +25,7 @@ export const generateMetadata = async ({
const title = t("meta.title")
const description = `${title} - ${t("meta.description")}`
const image = "/images/logo.webp"
const url = new URL("https://theoludwig.fr")
const url = new URL(`https://${DOMAIN}`)
const locales = LOCALES.join(", ")
return {
@ -74,6 +81,12 @@ const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => {
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
<Script
defer
data-domain={DOMAIN}
src="https://plausible.theoludwig.fr/js/script.js"
/>
</body>
</html>
)

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.1",
"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:",
@ -37,6 +37,7 @@
"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

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 KiB

View File

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

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,16 +1,14 @@
import { FlatCompat } from "@eslint/eslintrc"
import storybook from "eslint-plugin-storybook"
import tailwind from "eslint-plugin-tailwindcss"
import typescriptESLint from "typescript-eslint"
import config from "../eslint.config.js"
const flatCompat = new FlatCompat()
const flatCompat = new FlatCompat({
baseDirectory: import.meta.dirname,
})
export default typescriptESLint.config(
...config,
...flatCompat.extends("next/core-web-vitals"),
...tailwind.configs["flat/recommended"],
...storybook.configs["flat/recommended"],
{
name: "config-eslint/nextjs",
settings: {
@ -22,8 +20,6 @@ export default typescriptESLint.config(
},
},
rules: {
"tailwindcss/classnames-order": "off",
"tailwindcss/no-custom-classname": "off",
"@next/next/no-html-link-for-pages": "off",
"@next/next/no-img-element": "off",
"react/self-closing-comp": [

View File

@ -1,6 +1,6 @@
{
"name": "@repo/eslint-config",
"version": "4.1.1",
"name": "@repo/config-eslint",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"exports": {
@ -25,8 +25,6 @@
"eslint-plugin-promise": "catalog:",
"eslint-plugin-unicorn": "catalog:",
"eslint-config-next": "catalog:",
"eslint-plugin-storybook": "catalog:",
"eslint-plugin-tailwindcss": "catalog:",
"eslint-plugin-import-x": "catalog:",
"typescript": "catalog:",
"globals": "catalog:"

View File

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

View File

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

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.1",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"main": "./tailwind.config.js",
@ -21,12 +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

@ -1,16 +1,26 @@
@import "@fontsource/montserrat/400.css";
@import "@fontsource/montserrat/500.css";
@import "@fontsource/montserrat/600.css";
@import "@fontsource/montserrat/700.css";
@import "@fontsource/montserrat/800.css";
@import "tailwindcss";
@config "./tailwind.config.js";
@tailwind base;
@tailwind components;
@tailwind utilities;
@source "../../packages/ui";
@source "../../packages/blog";
@source "../../apps/website";
@source "../../apps/storybook/.storybook";
@import "@fontsource/montserrat/400.css" layer(base);
@import "@fontsource/montserrat/500.css" layer(base);
@import "@fontsource/montserrat/600.css" layer(base);
@import "@fontsource/montserrat/700.css" layer(base);
@import "@fontsource/montserrat/800.css" layer(base);
@layer base {
* {
min-width: 0;
[type="search"]::-webkit-search-decoration,
[type="search"]::-webkit-search-cancel-button {
appearance: none;
}
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
b,

View File

@ -1,5 +1,4 @@
import typographyPlugin from "@tailwindcss/typography"
import { fontFamily } from "tailwindcss/defaultTheme"
/** @type {Omit<import('tailwindcss').Config, "content">} */
const config = {
@ -29,7 +28,7 @@ const config = {
lightFlag: "0px 1px 10px rgba(0, 0, 0, 0.25)",
},
fontFamily: {
sans: ["'Montserrat'", ...fontFamily.sans],
sans: ["Montserrat", "sans-serif"],
},
typography: {
DEFAULT: {

View File

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

View File

@ -11,6 +11,7 @@
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,

View File

@ -1,29 +1,24 @@
{
"name": "repo",
"version": "4.1.1",
"version": "0.0.0-develop",
"private": true,
"type": "module",
"packageManager": "pnpm@9.14.4+sha512.c8180b3fbe4e4bca02c94234717896b5529740a6cbadf19fa78254270403ea2f27d4e1d46a08a0f56c89b63dc8ebfd3ee53326da720273794e6200fcf0d184ab",
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
"engines": {
"node": ">=22.0.0",
"pnpm": ">=9.14.4"
"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: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 +26,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

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

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.1",
"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:",
@ -49,6 +49,7 @@
"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:

View File

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

View File

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

View File

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

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

View File

@ -1,21 +1,22 @@
import deepmerge from "deepmerge"
import type { AbstractIntlMessages } from "next-intl"
import { hasLocale } from "next-intl"
import { getRequestConfig } from "next-intl/server"
import { routing } from "./routing.ts"
import type { Locale } from "@repo/utils/constants"
import { LOCALE_DEFAULT, LOCALES } from "@repo/utils/constants"
import { LOCALE_DEFAULT } from "@repo/utils/constants"
import { deepMerge } from "@repo/utils/objects"
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale
if (!LOCALES.includes(locale as Locale)) {
locale = LOCALE_DEFAULT
}
const requested = await requestLocale
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale
const userMessages = (await import(`./translations/${locale}.json`)).default
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": {
@ -131,6 +140,10 @@
"title": "Open-Source"
},
"portfolio": {
"fusey": {
"description": "ARK: Survival Ascended Wiki and Player stats tracker.",
"title": "Fusey"
},
"carolo": {
"description": "Strategy board game similar to chess which allows grandiose moves (only available in French).",
"title": "Carolo"
@ -150,13 +163,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": {
@ -131,6 +140,10 @@
"title": "Open-Source"
},
"portfolio": {
"fusey": {
"description": "ARK: Survival Ascended Wiki et suivi des statistiques des joueurs.",
"title": "Fusey"
},
"carolo": {
"description": "Jeu de plateau stratégique similaire aux échecs qui permet des coups grandioses, reposant sur des enchaînements remarquables.",
"title": "Carolo"
@ -150,13 +163,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.1",
"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

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

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.1",
"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:",
@ -51,6 +51,7 @@
"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

@ -26,9 +26,9 @@ export const OpenSource: React.FC<OpenSourceProps> = () => {
href="https://github.com/nodejs/node/commits?author=theoludwig"
/>
<Repository
name="standard/standard"
description="🌟 JavaScript Style Guide, with linter & automatic code fixer"
href="https://github.com/standard/standard/commits?author=theoludwig"
name="nodejs/nodejs.org"
description="The Node.js® Website"
href="https://github.com/nodejs/nodejs.org/commits/main/?author=theoludwig"
/>
<Repository
name="DefinitelyTyped/DefinitelyTyped"

View File

@ -9,18 +9,25 @@ export const Portfolio: React.FC<PortfolioProps> = () => {
const t = useTranslations()
const items: PortfolioProject[] = [
{
id: "fusey",
title: t("home.portfolio.fusey.title"),
description: t("home.portfolio.fusey.description"),
link: "https://fusey.gg",
image: "/images/portfolio/Fusey.webp",
},
{
id: "carolo",
title: t("home.portfolio.carolo.title"),
description: t("home.portfolio.carolo.description"),
link: "https://carolo.theoludwig.fr/",
link: "https://carolo.theoludwig.fr",
image: "/images/portfolio/Carolo.webp",
},
{
id: "leon",
title: t("home.portfolio.leon.title"),
description: t("home.portfolio.leon.description"),
link: "https://getleon.ai/",
link: "https://getleon.ai",
image: "/images/portfolio/Leon.webp",
},
]

View File

@ -33,7 +33,7 @@ export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
<div className="flex justify-center">
<Image
quality={100}
className="size-[300px] transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5"
className="size-[300px] rounded-xl transition-opacity duration-500 group-hover:opacity-20 dark:group-hover:opacity-5"
width={300}
height={300}
src={image}

View File

@ -2,7 +2,6 @@
import { classNames } from "@repo/config-tailwind/classNames"
import { usePathname, useRouter } from "@repo/i18n/routing"
import type { Locale } from "@repo/utils/constants"
import { LOCALES } from "@repo/utils/constants"
import { useLocale } from "next-intl"
import { useEffect, useRef } from "react"
@ -16,7 +15,7 @@ export interface LocalesProps {}
export const Locales: React.FC<LocalesProps> = () => {
const router = useRouter()
const pathname = usePathname()
const localeCurrent = useLocale() as Locale
const localeCurrent = useLocale()
const {
value: isVisibleMenu,

View File

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

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.1",
"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

@ -0,0 +1,19 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"
import { 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")
// Act - When
const output = getISODate(input)
// 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

@ -0,0 +1,43 @@
import assert from "node:assert/strict"
import { describe, it } from "node:test"
import { capitalize } from "../strings.ts"
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)
// 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 = ""
// Act - When
const output = capitalize(input)
// 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!"
// Act - When
const output = capitalize(input)
// 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

@ -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 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { getAge, getISODate } from "../dates.ts"
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)
// 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)
})
})

View File

@ -1,41 +0,0 @@
import { describe, expect, it } from "vitest"
import { capitalize } from "../strings.ts"
describe("capitalize", () => {
it("should capitalize the first letter of a string", () => {
// Arrange - Given
const input = "hello, world!"
// Act - When
const output = capitalize(input)
// Assert - Then
const expected = "Hello, world!"
expect(output).toEqual(expected)
})
it("should return an empty string when the input is an empty string", () => {
// Arrange - Given
const input = ""
// Act - When
const output = capitalize(input)
// Assert - Then
const expected = ""
expect(output).toEqual(expected)
})
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)
// Assert - Then
const expected = "Hello, world!"
expect(output).toEqual(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",
},
},
})

11131
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,104 +1,105 @@
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.5.3"
# Utils
"deepmerge": "4.3.1"
"mime": "4.0.7"
# React.js/Next.js
"next": "15.0.4"
"next-intl": "3.26.0"
"next-themes": "0.4.4"
"react": "19.0.0"
"react-dom": "19.0.0"
"react-icons": "5.4.0"
"@types/react": "19.0.0"
"@types/react-dom": "19.0.0"
"next": &next "15.3.2"
"next-intl": "4.1.0"
"next-themes": "0.4.6"
"react": "19.1.0"
"react-dom": "19.1.0"
"react-icons": "5.5.0"
"@types/react": "19.1.3"
"@types/react-dom": "19.1.3"
# Blog
"@giscus/react": "3.0.0"
"@giscus/react": "3.1.0"
"gray-matter": "4.0.3"
"katex": "0.16.11"
"katex": "0.16.22"
"next-mdx-remote": "5.0.0"
"@mdx-js/mdx": "3.1.0"
"rehype-katex": "7.0.1"
"rehype-raw": "7.0.0"
"rehype-slug": "6.0.0"
"remark-gfm": "4.0.0"
"remark-gfm": "4.0.1"
"remark-math": "6.0.0"
"shiki": "1.24.0"
"@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.18.0"
"markdownlint": "0.38.0"
"markdownlint-rule-relative-links": "4.1.0"
# TypeScript
"typescript": "5.7.2"
"typescript": "5.8.3"
"@total-typescript/ts-reset": "0.6.1"
"@types/node": "22.10.1"
"@types/node": "22.15.17"
# ESLint
"globals": "15.13.0"
"typescript-eslint": "8.17.0"
"@eslint/eslintrc": "3.2.0"
"eslint": "9.16.0"
"eslint-config-conventions": "17.0.1"
"globals": "16.1.0"
"typescript-eslint": "8.32.0"
"@eslint/eslintrc": "3.3.1"
"eslint": "9.26.0"
"eslint-config-conventions": "19.2.0"
"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-plugin-unicorn": "59.0.1"
"eslint-config-next": *next
"eslint-plugin-import-x": "4.11.1"
# Prettier
"prettier": "3.4.2"
"prettier-plugin-tailwindcss": "0.6.9"
"editorconfig-checker": "6.0.0"
"prettier": "3.5.3"
"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.6.12"
"@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.22.0"
"@chromatic-com/storybook": "3.2.6"
"chromatic": "11.28.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.52.0"
"@playwright/test": *playwright
"axe-playwright": "2.1.0"
"start-server-and-test": "2.0.11"
# CSS
"postcss": "8.4.49"
"tailwindcss": "3.4.16"
"@tailwindcss/typography": "0.5.15"
"tailwind-merge": "2.5.5"
"postcss": "8.5.3"
"@tailwindcss/postcss": "4.1.6"
"@tailwindcss/typography": "0.5.16"
"tailwindcss": "4.1.6"
"tailwind-merge": "3.2.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.2.5"
onlyBuiltDependencies:
- "@swc/core"
- "@tailwindcss/oxide"
- "core-js-pure"
- "esbuild"
- "sharp"
- "unrs-resolver"
publicHoistPattern:
- "*eslint*"
- "*prettier*"

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