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

chore: cleaner setup

This commit is contained in:
2025-02-08 20:00:47 +01:00
parent 270920111a
commit b63cc3a66e
69 changed files with 3393 additions and 5914 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -1,61 +0,0 @@
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()
export default typescriptESLint.config(
...config,
...flatCompat.extends("next/core-web-vitals"),
...tailwind.configs["flat/recommended"],
...storybook.configs["flat/recommended"],
{
name: "config-eslint/nextjs",
settings: {
tailwindcss: {
callees: ["classNames", "cva"],
},
react: {
version: "detect",
},
},
rules: {
"tailwindcss/classnames-order": "off",
"tailwindcss/no-custom-classname": "off",
"@next/next/no-html-link-for-pages": "off",
"@next/next/no-img-element": "off",
"react/self-closing-comp": [
"error",
{
component: true,
html: true,
},
],
"react/void-dom-elements-no-children": "error",
"react/jsx-boolean-value": "error",
"no-restricted-imports": [
"error",
{
paths: [
{
name: "next/link",
message: "Please import from `@repo/i18n/routing` instead.",
},
{
name: "next/navigation",
importNames: [
"redirect",
"permanentRedirect",
"useRouter",
"usePathname",
],
message: "Please import from `@repo/i18n/routing` instead.",
},
],
},
],
},
},
)

View File

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

View File

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

View File

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

View File

@ -1,3 +0,0 @@
import type { Config } from "tailwindcss"
export default Config

View File

@ -1,32 +0,0 @@
{
"name": "@repo/config-tailwind",
"version": "4.1.3",
"private": true,
"type": "module",
"main": "./tailwind.config.js",
"types": "./index.d.ts",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./tailwind.config.js",
"require": "./tailwind.config.js",
"default": "./tailwind.config.js"
},
"./classNames": "./classNames.ts",
"./styles.css": "./styles.css"
},
"dependencies": {
"@fontsource/montserrat": "catalog:",
"clsx": "catalog:",
"tailwind-merge": "catalog:"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@tailwindcss/typography": "catalog:",
"typescript-eslint": "catalog:",
"eslint": "catalog:",
"postcss": "catalog:",
"tailwindcss": "catalog:"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +0,0 @@
{
"name": "@repo/config-typescript",
"version": "4.1.3",
"private": true,
"files": [
"tsconfig.json"
]
}

View File

@ -1,29 +0,0 @@
{
"compilerOptions": {
"strict": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"esModuleInterop": true,
"allowImportingTsExtensions": true,
"skipLibCheck": true,
"jsx": "preserve",
"incremental": true,
"noEmit": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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