mirror of
https://github.com/theoludwig/theoludwig.git
synced 2025-05-29 22:37:44 +02:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
d795025860
|
|||
a43bfb4a0d
|
|||
69af1bccc3
|
|||
cec70161f7
|
|||
f70a66e251
|
|||
38eb296088
|
|||
43d91bfc28
|
|||
b63cc3a66e
|
|||
270920111a
|
|||
d91feb8de4
|
|||
e68cb08a6f
|
|||
09d677bd37
|
|||
db1159f20c
|
|||
af5c845e4b
|
|||
3f66dfe46e
|
|||
d52a0c6f08
|
@ -9,3 +9,6 @@ end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
indent_size = 4
|
||||
|
@ -1,2 +1,2 @@
|
||||
TZ=UTC
|
||||
TZ=Europe/Paris
|
||||
WEBSITE_PORT=3000
|
||||
|
4
.github/ISSUE_TEMPLATE/BUG.md
vendored
4
.github/ISSUE_TEMPLATE/BUG.md
vendored
@ -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
|
||||
|
10
.github/workflows/chromatic.yml
vendored
10
.github/workflows/chromatic.yml
vendored
@ -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"
|
||||
|
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@ -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"
|
||||
|
45
.github/workflows/release.yml
vendored
45
.github/workflows/release.yml
vendored
@ -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 }}
|
@ -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
19
.markdownlint-cli2.mjs
Normal file
@ -0,0 +1,19 @@
|
||||
import relativeLinksRule, { markdownIt } from "markdownlint-rule-relative-links"
|
||||
|
||||
const config = {
|
||||
config: {
|
||||
extends: "markdownlint/style/prettier",
|
||||
default: true,
|
||||
"relative-links": true,
|
||||
"no-duplicate-heading": false,
|
||||
"no-inline-html": false,
|
||||
},
|
||||
globs: ["**/*.md"],
|
||||
ignores: ["**/node_modules"],
|
||||
customRules: [relativeLinksRule],
|
||||
markdownItFactory: () => {
|
||||
return markdownIt
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
@ -1,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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@ -6,7 +6,6 @@
|
||||
"davidanson.vscode-markdownlint",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"mikestead.dotenv",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"antfu.pnpm-catalog-lens",
|
||||
"Lokalise.i18n-ally"
|
||||
]
|
||||
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -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"],
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.12.3 [(`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>
|
||||
|
15
README.md
15
README.md
@ -27,10 +27,21 @@
|
||||
"nationality": "Alsace, France",
|
||||
"interests": ["Developer Full Stack", "Open-Source Enthusiast"],
|
||||
"skills": {
|
||||
"programmingLanguages": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"],
|
||||
"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"]
|
||||
"tools": [
|
||||
"GNU/Linux",
|
||||
"Arch Linux",
|
||||
"Visual Studio Code",
|
||||
"Git",
|
||||
"Docker"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
5
apps/storybook/.postcssrc.json
Normal file
5
apps/storybook/.postcssrc.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugins": {
|
||||
"@tailwindcss/postcss": {}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
3
apps/storybook/.storybook/storybook-css-overrides.css
Normal file
3
apps/storybook/.storybook/storybook-css-overrides.css
Normal file
@ -0,0 +1,3 @@
|
||||
body {
|
||||
overflow: auto !important;
|
||||
}
|
@ -13,7 +13,8 @@ const config: TestRunnerConfig = {
|
||||
async postVisit(page, context) {
|
||||
const storyContext = await getStoryContext(page, context)
|
||||
|
||||
if (storyContext.parameters?.a11y?.disable) {
|
||||
const isA11yDisabled = storyContext.parameters?.a11y?.disable as boolean
|
||||
if (isA11yDisabled) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -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"],
|
||||
|
84
apps/storybook/http-server.ts
Normal file
84
apps/storybook/http-server.ts
Normal 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}\``)
|
||||
},
|
||||
)
|
@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "@repo/storybook",
|
||||
"version": "4.1.0",
|
||||
"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 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:"
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
@ -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],
|
||||
}
|
||||
|
||||
|
@ -3,5 +3,5 @@
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"]
|
||||
},
|
||||
"include": ["./.storybook/**/*.ts", "./.storybook/**/*.tsx"]
|
||||
"include": ["http-server.ts", "./.storybook/**/*.ts", "./.storybook/**/*.tsx"]
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"$schema": "../../node_modules/turbo/schema.json",
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"test": {
|
||||
|
@ -1,4 +1,4 @@
|
||||
TZ=UTC
|
||||
TZ=Europe/Paris
|
||||
HOSTNAME=0.0.0.0
|
||||
PORT=3000
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
5
apps/website/.postcssrc.json
Normal file
5
apps/website/.postcssrc.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugins": {
|
||||
"@tailwindcss/postcss": {}
|
||||
}
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
FROM node:22.11.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.2.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
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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"],
|
||||
|
2
apps/website/next-env.d.ts
vendored
2
apps/website/next-env.d.ts
vendored
@ -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.
|
||||
|
@ -9,10 +9,6 @@ const nextConfig = {
|
||||
unoptimized: true,
|
||||
},
|
||||
compress: false,
|
||||
|
||||
// https://github.com/hashicorp/next-mdx-remote/issues/436#issuecomment-2066971842
|
||||
transpilePackages: ["next-mdx-remote", "shiki"],
|
||||
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/website",
|
||||
"version": "4.1.0",
|
||||
"version": "0.0.0-develop",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
@ -19,13 +19,16 @@
|
||||
"@repo/utils": "workspace:*",
|
||||
"@repo/i18n": "workspace:*",
|
||||
"@repo/ui": "workspace:*",
|
||||
"@mdx-js/mdx": "catalog:",
|
||||
"next-mdx-remote": "catalog:",
|
||||
"shiki": "catalog:",
|
||||
"next": "catalog:",
|
||||
"next-intl": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/config-eslint": "workspace:*",
|
||||
"@repo/config-typescript": "workspace:*",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
@ -34,6 +37,7 @@
|
||||
"eslint": "catalog:",
|
||||
"postcss": "catalog:",
|
||||
"tailwindcss": "catalog:",
|
||||
"@tailwindcss/postcss": "catalog:",
|
||||
"typescript-eslint": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
BIN
apps/website/public/images/portfolio/Fusey.webp
Normal file
BIN
apps/website/public/images/portfolio/Fusey.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 945 KiB |
@ -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],
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -1,7 +1,6 @@
|
||||
import typescriptESLint from "typescript-eslint"
|
||||
import configConventions from "eslint-config-conventions"
|
||||
import importX from "eslint-plugin-import-x"
|
||||
import unicorn from "eslint-plugin-unicorn"
|
||||
|
||||
export default typescriptESLint.config(
|
||||
{
|
||||
@ -11,7 +10,6 @@ export default typescriptESLint.config(
|
||||
"**/eslint.config.js",
|
||||
"**/tailwind.config.js",
|
||||
"**/postcss.config.js",
|
||||
"**/vitest.config.ts",
|
||||
"**/kysely.config.ts",
|
||||
],
|
||||
},
|
||||
@ -20,7 +18,6 @@ export default typescriptESLint.config(
|
||||
name: "config-eslint",
|
||||
plugins: {
|
||||
"import-x": importX,
|
||||
unicorn,
|
||||
},
|
||||
rules: {
|
||||
"import-x/extensions": [
|
||||
@ -33,8 +30,6 @@ export default typescriptESLint.config(
|
||||
jsx: "never",
|
||||
},
|
||||
],
|
||||
"unicorn/explicit-length-check": "error",
|
||||
"unicorn/consistent-destructuring": "off",
|
||||
},
|
||||
},
|
||||
)
|
@ -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": [
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/eslint-config",
|
||||
"version": "4.1.0",
|
||||
"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:"
|
5
configs/config-tailwind/.postcssrc.json
Normal file
5
configs/config-tailwind/.postcssrc.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugins": {
|
||||
"@tailwindcss/postcss": {}
|
||||
}
|
||||
}
|
@ -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 => {
|
@ -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"],
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/config-tailwind",
|
||||
"version": "4.1.0",
|
||||
"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:"
|
||||
}
|
||||
}
|
@ -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,
|
||||
@ -155,10 +165,6 @@ code .line::before {
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code .line:last-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.katex .base {
|
||||
display: inline !important;
|
||||
white-space: normal !important;
|
@ -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: {
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/config-typescript",
|
||||
"version": "4.1.0",
|
||||
"version": "0.0.0-develop",
|
||||
"private": true,
|
||||
"files": [
|
||||
"tsconfig.json"
|
@ -11,6 +11,7 @@
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
17
package.json
17
package.json
@ -1,29 +1,24 @@
|
||||
{
|
||||
"name": "repo",
|
||||
"version": "4.1.0",
|
||||
"version": "0.0.0-develop",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee",
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
"pnpm": ">=9.12.3"
|
||||
"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:"
|
||||
}
|
||||
|
5
packages/blog/.postcssrc.json
Normal file
5
packages/blog/.postcssrc.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugins": {
|
||||
"@tailwindcss/postcss": {}
|
||||
}
|
||||
}
|
@ -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"],
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/blog",
|
||||
"version": "4.1.0",
|
||||
"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:"
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
}
|
||||
|
||||
export default 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>
|
||||
|
@ -103,13 +103,14 @@ export const BlogPostContent: React.FC<BlogPostContentProps> = async (
|
||||
},
|
||||
a: (props) => {
|
||||
const { href = "", ...rest } = props
|
||||
if (href.startsWith("#")) {
|
||||
const hrefString = href as string
|
||||
if (hrefString.startsWith("#")) {
|
||||
return <a {...props} />
|
||||
}
|
||||
if (href.startsWith("../posts/")) {
|
||||
if (hrefString.startsWith("../posts/")) {
|
||||
return (
|
||||
<Link
|
||||
href={href
|
||||
href={hrefString
|
||||
.replace("../posts/", "/blog/")
|
||||
.replace(".md", "")}
|
||||
{...rest}
|
||||
|
@ -2,7 +2,6 @@ import sharedConfig from "@repo/config-tailwind"
|
||||
|
||||
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
|
||||
const config = {
|
||||
content: ["./src/**/*.tsx"],
|
||||
presets: [sharedConfig],
|
||||
}
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
@ -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"],
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/i18n",
|
||||
"version": "4.1.0",
|
||||
"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:"
|
||||
}
|
||||
}
|
||||
|
15
packages/i18n/src/messages.d.ts
vendored
15
packages/i18n/src/messages.d.ts
vendored
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
})
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
typecheck: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
})
|
@ -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"],
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/react-hooks",
|
||||
"version": "4.1.0",
|
||||
"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:"
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
@ -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)
|
||||
})
|
||||
})
|
@ -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)
|
||||
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
})
|
5
packages/ui/.postcssrc.json
Normal file
5
packages/ui/.postcssrc.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugins": {
|
||||
"@tailwindcss/postcss": {}
|
||||
}
|
||||
}
|
@ -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"],
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@repo/ui",
|
||||
"version": "4.1.0",
|
||||
"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:"
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
@ -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"
|
||||
|
@ -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",
|
||||
},
|
||||
]
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -2,7 +2,6 @@ import sharedConfig from "@repo/config-tailwind"
|
||||
|
||||
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
|
||||
const config = {
|
||||
content: ["./src/**/*.tsx"],
|
||||
presets: [sharedConfig],
|
||||
}
|
||||
|
||||
|
@ -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"],
|
||||
|
@ -1,29 +1,28 @@
|
||||
{
|
||||
"name": "@repo/utils",
|
||||
"version": "4.1.0",
|
||||
"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 --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:"
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,9 @@ export const LOCALE_PREFIX = "never"
|
||||
|
||||
export const THEMES = ["light", "dark"] as const
|
||||
export type Theme = (typeof THEMES)[number]
|
||||
export const THEME_DEFAULT = "light" as Theme
|
||||
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"
|
||||
|
19
packages/utils/src/objects.ts
Normal file
19
packages/utils/src/objects.ts
Normal 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
|
||||
}
|
19
packages/utils/src/test/dates.test.ts
Normal file
19
packages/utils/src/test/dates.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
85
packages/utils/src/test/objects.test.ts
Normal file
85
packages/utils/src/test/objects.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
43
packages/utils/src/test/strings.test.ts
Normal file
43
packages/utils/src/test/strings.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
80
packages/utils/src/test/urls.test.ts
Normal file
80
packages/utils/src/test/urls.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
@ -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)
|
||||
})
|
||||
})
|
@ -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)
|
||||
})
|
||||
})
|
@ -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)
|
||||
})
|
||||
})
|
49
packages/utils/src/types.ts
Normal file
49
packages/utils/src/types.ts
Normal 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>)
|
18
packages/utils/src/urls.ts
Normal file
18
packages/utils/src/urls.ts
Normal 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
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
coverage: {
|
||||
enabled: true,
|
||||
provider: "v8",
|
||||
},
|
||||
},
|
||||
})
|
12090
pnpm-lock.yaml
generated
12090
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user