mirror of
https://github.com/theoludwig/theoludwig.git
synced 2026-08-25 09:21:33 +02:00
Compare commits
2
Commits
02296281f3
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
909468fef2
|
||
|
|
3f0323366d
|
@@ -18,16 +18,16 @@ jobs:
|
|||||||
NEXT_TELEMETRY_DISABLED: "1"
|
NEXT_TELEMETRY_DISABLED: "1"
|
||||||
STORYBOOK_DISABLE_TELEMETRY: "1"
|
STORYBOOK_DISABLE_TELEMETRY: "1"
|
||||||
steps:
|
steps:
|
||||||
- uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
|
- uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" # v7.0.1
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: "pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093" # v6.0.8
|
- uses: "pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86" # v6.0.10
|
||||||
|
|
||||||
- name: "Setup Node.js"
|
- name: "Setup Node.js"
|
||||||
uses: "actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e" # v6.4.0
|
uses: "actions/setup-node@820762786026740c76f36085b0efc47a31fe5020" # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: "24.15.0"
|
node-version: "26.7.0"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: "node --version"
|
- run: "node --version"
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
run: "pnpm clean-install"
|
run: "pnpm clean-install"
|
||||||
|
|
||||||
- name: "Run Chromatic"
|
- name: "Run Chromatic"
|
||||||
uses: "chromaui/action@a200f3ba3ff81232c47ac7942347fb212b1a67dc" # latest
|
uses: "chromaui/action@534eebfc19023579541d106f7b61d5ad70ed65c7" # latest
|
||||||
with:
|
with:
|
||||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
workingDir: "apps/storybook"
|
workingDir: "apps/storybook"
|
||||||
|
|||||||
@@ -18,14 +18,16 @@ jobs:
|
|||||||
NEXT_TELEMETRY_DISABLED: "1"
|
NEXT_TELEMETRY_DISABLED: "1"
|
||||||
STORYBOOK_DISABLE_TELEMETRY: "1"
|
STORYBOOK_DISABLE_TELEMETRY: "1"
|
||||||
steps:
|
steps:
|
||||||
- uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
|
- uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1" # v7.0.1
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: "pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093" # v6.0.8
|
- uses: "pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86" # v6.0.10
|
||||||
|
|
||||||
- name: "Setup Node.js"
|
- name: "Setup Node.js"
|
||||||
uses: "actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e" # v6.4.0
|
uses: "actions/setup-node@820762786026740c76f36085b0efc47a31fe5020" # v7.0.0
|
||||||
with:
|
with:
|
||||||
node-version: "24.15.0"
|
node-version: "26.7.0"
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: "node --version"
|
- run: "node --version"
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,15 @@
|
|||||||
"unicorn/no-useless-undefined": "off",
|
"unicorn/no-useless-undefined": "off",
|
||||||
"unicorn/no-object-as-default-parameter": "off",
|
"unicorn/no-object-as-default-parameter": "off",
|
||||||
|
|
||||||
|
"react/unsupported-syntax": "error",
|
||||||
|
"react/no-deriving-state-in-effects": "error",
|
||||||
|
"react/rule-suppression": "error",
|
||||||
|
"react/syntax": "error",
|
||||||
|
"react/todo": "error",
|
||||||
|
"react/invariant": "error",
|
||||||
|
"react/capitalized-calls": "error",
|
||||||
|
"react/exhaustive-effect-dependencies": "error",
|
||||||
|
"react/memo-dependencies": "error",
|
||||||
"react/no-array-index-key": "off",
|
"react/no-array-index-key": "off",
|
||||||
"react/no-unknown-property": "off",
|
"react/no-unknown-property": "off",
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/) >= v24.0.0 [(`nvm install 24`)](https://nvm.sh)
|
- [Node.js](https://nodejs.org/) >= v26.1.0 [(`nvm install 26`)](https://nvm.sh)
|
||||||
- [pnpm](https://pnpm.io/) [(`npm install --global corepack@0.35.0 && corepack enable`)](https://github.com/nodejs/corepack)
|
- [pnpm](https://pnpm.io/) [(`npm install --global corepack@0.35.0 && corepack enable`)](https://github.com/nodejs/corepack)
|
||||||
- [Docker](https://www.docker.com/)
|
- [Docker](https://www.docker.com/)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const args = util.parseArgs({
|
|||||||
const host = args.values.host
|
const host = args.values.host
|
||||||
const basePath = args.values.path
|
const basePath = args.values.path
|
||||||
|
|
||||||
const port = Number.parseInt(args.values.port, 10)
|
const port = Math.trunc(Number(args.values.port))
|
||||||
if (Number.isNaN(port)) {
|
if (Number.isNaN(port)) {
|
||||||
console.error("Error: Invalid port number.")
|
console.error("Error: Invalid port number.")
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM docker.io/node:24.15.0-slim@sha256:879b21aec4a1ad820c27ccd565e7c7ed955f24b92e6694556154f251e4bdb240 AS node-pnpm
|
FROM docker.io/node:26.7.0-slim@sha256:4ebb5ace66f15a24c14c492e01a8beeed4fddf970a856109f5126e703e5fe503 AS node-pnpm
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME/bin:$PATH"
|
ENV PATH="$PNPM_HOME/bin:$PATH"
|
||||||
RUN npm install --global corepack@0.35.0 && corepack enable
|
RUN npm install --global corepack@0.35.0 && corepack enable
|
||||||
@@ -9,7 +9,7 @@ WORKDIR /usr/src/app
|
|||||||
|
|
||||||
FROM node-pnpm AS builder
|
FROM node-pnpm AS builder
|
||||||
COPY ./ ./
|
COPY ./ ./
|
||||||
RUN pnpm install --global turbo@2.9.16
|
RUN pnpm install --global turbo@2.10.11
|
||||||
RUN turbo prune @repo/website --docker
|
RUN turbo prune @repo/website --docker
|
||||||
|
|
||||||
FROM node-pnpm AS installer
|
FROM node-pnpm AS installer
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ import { notFound } from "next/navigation"
|
|||||||
|
|
||||||
import { getBlogPostBySlug, getBlogPosts } from "@repo/blog"
|
import { getBlogPostBySlug, getBlogPosts } from "@repo/blog"
|
||||||
import { BlogPostUI } from "@repo/blog/BlogPostUI"
|
import { BlogPostUI } from "@repo/blog/BlogPostUI"
|
||||||
import type { Locale } from "@repo/utils/constants"
|
|
||||||
import { setRequestLocale } from "next-intl/server"
|
|
||||||
|
|
||||||
interface BlogPostPageProps {
|
interface BlogPostPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
slug: string
|
slug: string
|
||||||
locale: Locale
|
|
||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,9 +44,7 @@ export const generateStaticParams = async (): Promise<Array<{ slug: string }>> =
|
|||||||
const BlogPostPage: React.FC<BlogPostPageProps> = async (props) => {
|
const BlogPostPage: React.FC<BlogPostPageProps> = async (props) => {
|
||||||
const { params } = props
|
const { params } = props
|
||||||
|
|
||||||
const { locale, slug } = await params
|
const { slug } = await params
|
||||||
// Enable static rendering
|
|
||||||
setRequestLocale(locale)
|
|
||||||
|
|
||||||
const blogPost = await getBlogPostBySlug(slug)
|
const blogPost = await getBlogPostBySlug(slug)
|
||||||
if (blogPost == null) {
|
if (blogPost == null) {
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { getBlogPosts } from "@repo/blog"
|
import { getBlogPosts } from "@repo/blog"
|
||||||
import { BlogPosts } from "@repo/blog/BlogPosts"
|
import { BlogPosts } from "@repo/blog/BlogPosts"
|
||||||
import type { LocaleProps } from "@repo/i18n/routing"
|
|
||||||
import { MainLayout } from "@repo/ui/Layout/MainLayout"
|
import { MainLayout } from "@repo/ui/Layout/MainLayout"
|
||||||
import { Section, SectionDescription, SectionTitle } from "@repo/ui/Layout/Section"
|
import { Section, SectionDescription, SectionTitle } from "@repo/ui/Layout/Section"
|
||||||
import { LOCALE_DEFAULT } from "@repo/utils/constants"
|
import { LOCALE_DEFAULT } from "@repo/utils/constants"
|
||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
import { setRequestLocale } from "next-intl/server"
|
|
||||||
|
|
||||||
const title = "Blog | Théo LUDWIG"
|
const title = "Blog | Théo LUDWIG"
|
||||||
const description = "The latest news about my journey of learning computer science."
|
const description = "The latest news about my journey of learning computer science."
|
||||||
@@ -26,15 +24,7 @@ export const generateMetadata = async (): Promise<Metadata> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BlogPageProps extends LocaleProps {}
|
const BlogPage: React.FC = async () => {
|
||||||
|
|
||||||
const BlogPage: React.FC<BlogPageProps> = async (props) => {
|
|
||||||
const { params } = props
|
|
||||||
|
|
||||||
const { locale } = await params
|
|
||||||
// Enable static rendering
|
|
||||||
setRequestLocale(locale)
|
|
||||||
|
|
||||||
const posts = await getBlogPosts()
|
const posts = await getBlogPosts()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
import "@repo/config-tailwind/styles.css"
|
import "@repo/config-tailwind/styles.css"
|
||||||
import type { Locale } from "@repo/utils/constants"
|
|
||||||
import { Footer } from "@repo/ui/Layout/Footer"
|
import { Footer } from "@repo/ui/Layout/Footer"
|
||||||
import { Header } from "@repo/ui/Layout/Header"
|
import { Header } from "@repo/ui/Layout/Header"
|
||||||
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
|
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
|
||||||
import { VERSION } from "@repo/utils/constants"
|
import { VERSION } from "@repo/utils/constants"
|
||||||
import { setRequestLocale } from "next-intl/server"
|
|
||||||
|
|
||||||
interface MainLayoutProps extends React.PropsWithChildren {
|
interface MainLayoutProps extends React.PropsWithChildren {}
|
||||||
params: Promise<{
|
|
||||||
locale: string
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
const MainLayout: React.FC<MainLayoutProps> = async (props) => {
|
const MainLayout: React.FC<MainLayoutProps> = (props) => {
|
||||||
const { children, params } = props
|
const { children } = props
|
||||||
|
|
||||||
const { locale } = await params
|
|
||||||
// Enable static rendering
|
|
||||||
setRequestLocale(locale as Locale)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { LocaleProps } from "@repo/i18n/routing"
|
|
||||||
import { About } from "@repo/ui/Home/About"
|
import { About } from "@repo/ui/Home/About"
|
||||||
import { Interests } from "@repo/ui/Home/Interests"
|
import { Interests } from "@repo/ui/Home/Interests"
|
||||||
import { OpenSource } from "@repo/ui/Home/OpenSource"
|
import { OpenSource } from "@repo/ui/Home/OpenSource"
|
||||||
@@ -6,17 +5,8 @@ import { Portfolio } from "@repo/ui/Home/Portfolio"
|
|||||||
import { Skills } from "@repo/ui/Home/Skills"
|
import { Skills } from "@repo/ui/Home/Skills"
|
||||||
import { MainLayout } from "@repo/ui/Layout/MainLayout"
|
import { MainLayout } from "@repo/ui/Layout/MainLayout"
|
||||||
import { RevealFade } from "@repo/ui/Layout/Section"
|
import { RevealFade } from "@repo/ui/Layout/Section"
|
||||||
import { setRequestLocale } from "next-intl/server"
|
|
||||||
|
|
||||||
interface HomePageProps extends LocaleProps {}
|
|
||||||
|
|
||||||
const HomePage: React.FC<HomePageProps> = async (props) => {
|
|
||||||
const { params } = props
|
|
||||||
|
|
||||||
const { locale } = await params
|
|
||||||
// Enable static rendering
|
|
||||||
setRequestLocale(locale)
|
|
||||||
|
|
||||||
|
const HomePage: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<About />
|
<About />
|
||||||
|
|||||||
@@ -1,20 +1,10 @@
|
|||||||
import "@repo/config-tailwind/styles.css"
|
import "@repo/config-tailwind/styles.css"
|
||||||
import type { Locale } from "@repo/utils/constants"
|
|
||||||
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
|
import { ThemeProvider } from "@repo/ui/Layout/Header/SwitchTheme"
|
||||||
import { setRequestLocale } from "next-intl/server"
|
|
||||||
|
|
||||||
interface CurriculumVitaeLayoutProps extends React.PropsWithChildren {
|
interface CurriculumVitaeLayoutProps extends React.PropsWithChildren {}
|
||||||
params: Promise<{
|
|
||||||
locale: string
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
const CurriculumVitaeLayout: React.FC<CurriculumVitaeLayoutProps> = async (props) => {
|
const CurriculumVitaeLayout: React.FC<CurriculumVitaeLayoutProps> = (props) => {
|
||||||
const { children, params } = props
|
const { children } = props
|
||||||
|
|
||||||
const { locale } = await params
|
|
||||||
// Enable static rendering
|
|
||||||
setRequestLocale(locale as Locale)
|
|
||||||
|
|
||||||
return <ThemeProvider forcedTheme="light">{children}</ThemeProvider>
|
return <ThemeProvider forcedTheme="light">{children}</ThemeProvider>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import type { LocaleProps } from "@repo/i18n/routing"
|
|
||||||
import { CurriculumVitae } from "@repo/ui/CurriculumVitae"
|
import { CurriculumVitae } from "@repo/ui/CurriculumVitae"
|
||||||
import { setRequestLocale } from "next-intl/server"
|
|
||||||
|
|
||||||
interface CurriculumVitaeProps extends LocaleProps {}
|
|
||||||
|
|
||||||
const CurriculumVitaePage: React.FC<CurriculumVitaeProps> = async (props) => {
|
|
||||||
const { params } = props
|
|
||||||
|
|
||||||
const { locale } = await params
|
|
||||||
// Enable static rendering
|
|
||||||
setRequestLocale(locale)
|
|
||||||
|
|
||||||
|
const CurriculumVitaePage: React.FC = () => {
|
||||||
return <CurriculumVitae />
|
return <CurriculumVitae />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Locale } from "@repo/utils/constants"
|
|||||||
import { LOCALES } from "@repo/utils/constants"
|
import { LOCALES } from "@repo/utils/constants"
|
||||||
import type { Metadata, Viewport } from "next"
|
import type { Metadata, Viewport } from "next"
|
||||||
import { NextIntlClientProvider } from "next-intl"
|
import { NextIntlClientProvider } from "next-intl"
|
||||||
import { getMessages, getTranslations, setRequestLocale } from "next-intl/server"
|
import { getLocale, getMessages, getTranslations } from "next-intl/server"
|
||||||
import Script from "next/script"
|
import Script from "next/script"
|
||||||
|
|
||||||
const DOMAIN = "theoludwig.fr"
|
const DOMAIN = "theoludwig.fr"
|
||||||
@@ -12,15 +12,8 @@ export const viewport: Viewport = {
|
|||||||
themeColor: "#00aeff",
|
themeColor: "#00aeff",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const generateMetadata = async ({
|
export const generateMetadata = async (): Promise<Metadata> => {
|
||||||
params,
|
const t = await getTranslations()
|
||||||
}: {
|
|
||||||
params: Promise<{
|
|
||||||
locale: string
|
|
||||||
}>
|
|
||||||
}): Promise<Metadata> => {
|
|
||||||
const { locale } = await params
|
|
||||||
const t = await getTranslations({ locale: locale as Locale })
|
|
||||||
const title = t("meta.title")
|
const title = t("meta.title")
|
||||||
const description = `${title} - ${t("meta.description")}`
|
const description = `${title} - ${t("meta.description")}`
|
||||||
const image = "/images/logo.webp"
|
const image = "/images/logo.webp"
|
||||||
@@ -63,19 +56,12 @@ export const generateStaticParams = (): Array<{ locale: Locale }> => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LocaleLayoutProps extends React.PropsWithChildren {
|
interface LocaleLayoutProps extends React.PropsWithChildren {}
|
||||||
params: Promise<{
|
|
||||||
locale: string
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => {
|
const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => {
|
||||||
const { children, params } = props
|
const { children } = props
|
||||||
|
|
||||||
const { locale } = await params
|
|
||||||
// Enable static rendering
|
|
||||||
setRequestLocale(locale as Locale)
|
|
||||||
|
|
||||||
|
const locale = await getLocale()
|
||||||
const messages = await getMessages()
|
const messages = await getMessages()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
interface RootLayoutProps extends React.PropsWithChildren {}
|
|
||||||
|
|
||||||
const RootLayout = ({ children }: RootLayoutProps): React.ReactNode => {
|
|
||||||
return children
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RootLayout
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import Error from "next/error"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the default Next.js 404 page when a route
|
|
||||||
* is requested that doesn't match the middleware and
|
|
||||||
* therefore doesn't have a locale associated with it.
|
|
||||||
*/
|
|
||||||
const NotFound: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<body>
|
|
||||||
<Error statusCode={404} />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NotFound
|
|
||||||
@@ -13,3 +13,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
PORT: ${WEBSITE_PORT-3000}
|
PORT: ${WEBSITE_PORT-3000}
|
||||||
env_file: "./apps/website/.env"
|
env_file: "./apps/website/.env"
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10M"
|
||||||
|
max-file: "3"
|
||||||
|
|||||||
+2
-2
@@ -27,7 +27,7 @@
|
|||||||
"typescript": "catalog:"
|
"typescript": "catalog:"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=26.1.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.5.0+sha512.dbfcc4f81cf48597afd4bc391ffdf12c11f1a9fb83a395bfa6b0a2d9cc2fd8ffebafdb1ccbd529632153f793904c2615b7f09fe1a345473fd1c35845172a8eb1"
|
"packageManager": "pnpm@11.23.0+sha512.f00082e5b283a199b74e079da28d155c008fe232f44c8a06ea7ddfa014ecf719fc362f790ecc67b28106ddac2afb24c49a9a079159da560b2ce7d1e98efd11af"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import rehypeSlug from "rehype-slug"
|
|||||||
import remarkGfm from "remark-gfm"
|
import remarkGfm from "remark-gfm"
|
||||||
import remarkMath from "remark-math"
|
import remarkMath from "remark-math"
|
||||||
|
|
||||||
import { Link } from "@repo/i18n/routing"
|
import { Link } from "@repo/i18n/navigation"
|
||||||
import "katex/dist/katex.min.css"
|
import "katex/dist/katex.min.css"
|
||||||
import { BlogPostComments } from "./BlogPostComments.tsx"
|
import { BlogPostComments } from "./BlogPostComments.tsx"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Link } from "@repo/i18n/routing"
|
import { Link } from "@repo/i18n/navigation"
|
||||||
import { Typography } from "@repo/ui/Design/Typography"
|
import { Typography } from "@repo/ui/Design/Typography"
|
||||||
import { Section, SectionContent } from "@repo/ui/Layout/Section"
|
import { Section, SectionContent } from "@repo/ui/Layout/Section"
|
||||||
import { getISODate } from "@repo/utils/dates"
|
import { getISODate } from "@repo/utils/dates"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
"./translations/*.json": "./src/translations/*.json",
|
"./translations/*.json": "./src/translations/*.json",
|
||||||
"./messages.d.ts": "./src/messages.d.ts",
|
"./messages.d.ts": "./src/messages.d.ts",
|
||||||
|
"./navigation": "./src/navigation.ts",
|
||||||
"./request": "./src/request.ts",
|
"./request": "./src/request.ts",
|
||||||
"./routing": "./src/routing.ts"
|
"./routing": "./src/routing.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { createNavigation } from "next-intl/navigation"
|
||||||
|
|
||||||
|
import { routing } from "./routing.ts"
|
||||||
|
|
||||||
|
export const { Link, redirect, usePathname, useRouter, getPathname, permanentRedirect } =
|
||||||
|
createNavigation(routing)
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { hasLocale } from "next-intl"
|
import { hasLocale } from "next-intl"
|
||||||
import { getRequestConfig } from "next-intl/server"
|
import { getRequestConfig } from "next-intl/server"
|
||||||
|
import * as rootParams from "next/root-params"
|
||||||
import { routing } from "./routing.ts"
|
import { routing } from "./routing.ts"
|
||||||
|
|
||||||
import { LOCALE_DEFAULT } from "@repo/utils/constants"
|
import { LOCALE_DEFAULT } from "@repo/utils/constants"
|
||||||
import { deepMerge } from "@repo/utils/objects"
|
import { deepMerge } from "@repo/utils/objects"
|
||||||
|
|
||||||
export default getRequestConfig(async ({ requestLocale }) => {
|
export default getRequestConfig(async ({ locale: localeOverride }) => {
|
||||||
const requested = await requestLocale
|
const requested = localeOverride ?? (await rootParams.locale())
|
||||||
const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale
|
const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale
|
||||||
|
|
||||||
const userMessages = (await import(`./translations/${locale}.json`)).default
|
const userMessages = (await import(`./translations/${locale}.json`)).default
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
export function locale(): Promise<string | undefined>
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import { createNavigation } from "next-intl/navigation"
|
|
||||||
|
|
||||||
import { LOCALES, LOCALE_DEFAULT, LOCALE_PREFIX } from "@repo/utils/constants"
|
import { LOCALES, LOCALE_DEFAULT, LOCALE_PREFIX } from "@repo/utils/constants"
|
||||||
import { defineRouting } from "next-intl/routing"
|
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
|
// 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
|
// Country flag picture: https://purecatamphetamine.github.io/country-flag-icons/3x2/US.svg
|
||||||
@@ -11,17 +8,8 @@ import type { Locale } from "@repo/utils/constants"
|
|||||||
// Locale code is a combination of ISO 639-1 language code and ISO 3166-1 country code.
|
// 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.
|
// For example, `fr-FR` is a locale code for French language in France.
|
||||||
|
|
||||||
export interface LocaleProps {
|
|
||||||
params: Promise<{
|
|
||||||
locale: Locale
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const routing = defineRouting({
|
export const routing = defineRouting({
|
||||||
locales: LOCALES,
|
locales: LOCALES,
|
||||||
defaultLocale: LOCALE_DEFAULT,
|
defaultLocale: LOCALE_DEFAULT,
|
||||||
localePrefix: LOCALE_PREFIX,
|
localePrefix: LOCALE_PREFIX,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const { Link, redirect, usePathname, useRouter, getPathname, permanentRedirect } =
|
|
||||||
createNavigation(routing)
|
|
||||||
|
|||||||
@@ -90,8 +90,8 @@
|
|||||||
},
|
},
|
||||||
"interests": {
|
"interests": {
|
||||||
"title": "Interests & hobbies",
|
"title": "Interests & hobbies",
|
||||||
"fusey": "<link>Fusey (fusey.gg)</link>: website I'm developing for the game ARK that tracks the number of players connected to the servers in real time and has <strong>over ~5,000 visitors each month, ~100,000 members on Discord, and ~120,000 followers on X/Twitter</strong>.",
|
"fusey": "<link>Fusey (fusey.gg)</link>: company I co-founded with two partners. <strong>Find creators. Fill your community.</strong> A platform to find and book creators to promote a gaming community.",
|
||||||
"open-source": "<strong>Open-Source Contributor</strong>: publishing and <strong>contributing to npm packages</strong> and <link-github>GitHub</link-github> repositories, notably with <link-markdownlint>markdownlint-rule-relative-links</link-markdownlint> (around 20,000 downloads per week), contributions to <link-leon>Leon</link-leon>, an open-source personal assistant, among other projects."
|
"open-source": "<strong>Open-Source Contributor</strong>: publishing and <strong>contributing to npm packages</strong> and <link-github>GitHub</link-github> repositories, notably with <link-markdownlint>markdownlint-rule-relative-links</link-markdownlint> (around 20,000 downloads per week) among other projects."
|
||||||
},
|
},
|
||||||
"work": {
|
"work": {
|
||||||
"ircad": {
|
"ircad": {
|
||||||
@@ -107,6 +107,11 @@
|
|||||||
"feature-architecture": "IRCAD Core project architecture in <strong>TypeScript Monorepo</strong> with Turborepo, <strong>automatic deployment (CI/CD)</strong> and <strong>self-hosted internally with Docker Compose</strong>."
|
"feature-architecture": "IRCAD Core project architecture in <strong>TypeScript Monorepo</strong> with Turborepo, <strong>automatic deployment (CI/CD)</strong> and <strong>self-hosted internally with Docker Compose</strong>."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ircad-taiwan": {
|
||||||
|
"duration": "2 months",
|
||||||
|
"position": "Full Stack Web Developer Intern",
|
||||||
|
"summary": "Development of a participants management application and their hotel reservations."
|
||||||
|
},
|
||||||
"numerize": {
|
"numerize": {
|
||||||
"duration": "3 months",
|
"duration": "3 months",
|
||||||
"position": "Full Stack Web Developer Intern",
|
"position": "Full Stack Web Developer Intern",
|
||||||
|
|||||||
@@ -90,8 +90,8 @@
|
|||||||
},
|
},
|
||||||
"interests": {
|
"interests": {
|
||||||
"title": "Intérêts & loisirs",
|
"title": "Intérêts & loisirs",
|
||||||
"fusey": "<link>Fusey (fusey.gg)</link> : site web que je développe pour le jeu ARK qui permet de suivre en temps réel le nombre de joueurs connectés sur les serveurs et a plus de <strong>~5 000 visiteurs chaque mois, ~100 000 membres sur Discord et ~120 000 followers sur X/Twitter</strong>.",
|
"fusey": "<link>Fusey (fusey.gg)</link> : entreprise que j'ai cofondée avec deux associés. <strong>Trouvez des créateurs. Remplissez votre communauté.</strong> Une plateforme pour trouver et réserver des créateurs de contenu afin de promouvoir une communauté gaming.",
|
||||||
"open-source": "<strong>Contributeur de l'Open-Source</strong> : publications et <strong>contributions sur npm</strong> et <link-github>GitHub</link-github> avec notamment <link-markdownlint>markdownlint-rule-relative-links</link-markdownlint> avec ~20 000 téléchargements par semaine, contributions à <link-leon>Leon</link-leon>, un assistant personnel open source, parmis d'autres projets."
|
"open-source": "<strong>Contributeur de l'Open-Source</strong> : publications et <strong>contributions sur npm</strong> et <link-github>GitHub</link-github> avec notamment <link-markdownlint>markdownlint-rule-relative-links</link-markdownlint> avec ~20 000 téléchargements par semaine parmis d'autres projets."
|
||||||
},
|
},
|
||||||
"work": {
|
"work": {
|
||||||
"ircad": {
|
"ircad": {
|
||||||
@@ -107,6 +107,11 @@
|
|||||||
"feature-architecture": "Architecture du projet IRCAD Core en <strong>Monorepo TypeScript</strong> avec Turborepo, <strong>déploiement automatique (CI/CD)</strong> et <strong>hébergé en interne avec Docker Compose</strong>."
|
"feature-architecture": "Architecture du projet IRCAD Core en <strong>Monorepo TypeScript</strong> avec Turborepo, <strong>déploiement automatique (CI/CD)</strong> et <strong>hébergé en interne avec Docker Compose</strong>."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ircad-taiwan": {
|
||||||
|
"duration": "2 mois",
|
||||||
|
"position": "Stagiaire Développeur Web Full Stack",
|
||||||
|
"summary": "Développement d'une application de gestion des participants et de leurs réservations d'hôtel."
|
||||||
|
},
|
||||||
"numerize": {
|
"numerize": {
|
||||||
"duration": "3 mois",
|
"duration": "3 mois",
|
||||||
"position": "Stagiaire Développeur Web Full Stack",
|
"position": "Stagiaire Développeur Web Full Stack",
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
"extends": "@repo/config-typescript/tsconfig.json",
|
"extends": "@repo/config-typescript/tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
"types": ["@total-typescript/ts-reset"]
|
"types": ["@total-typescript/ts-reset"],
|
||||||
|
"paths": {
|
||||||
|
"next/root-params": ["./src/root-params.d.ts"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,6 @@ export const CurriculumVitaeInterests: React.FC<CurriculumVitaeInterestsProps> =
|
|||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
"link-leon": (children) => {
|
|
||||||
return (
|
|
||||||
<a href="https://github.com/leon-ai/leon" target="_blank" className="font-semibold">
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
"link-markdownlint": (children) => {
|
"link-markdownlint": (children) => {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Link } from "@repo/i18n/routing"
|
import { Link } from "@repo/i18n/navigation"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import { BirthDate } from "../Home/About/AboutList/BirthDate.tsx"
|
import { BirthDate } from "../Home/About/AboutList/BirthDate.tsx"
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ export const CurriculumVitaeWork: React.FC<CurriculumVitaeWorkProps> = () => {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
summary: t("curriculum-vitae.work.ircad-taiwan.summary"),
|
||||||
|
website: "https://ircadtaiwan.com.tw",
|
||||||
|
name: "IRCAD Taiwan",
|
||||||
|
location: "Taiwan",
|
||||||
|
position: t("curriculum-vitae.work.ircad-taiwan.position"),
|
||||||
|
dates: "20/07/2026 - 21/09/2026",
|
||||||
|
duration: t("curriculum-vitae.work.ircad-taiwan.duration"),
|
||||||
|
tasks: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
summary: t("curriculum-vitae.work.numerize.summary"),
|
summary: t("curriculum-vitae.work.numerize.summary"),
|
||||||
website: "https://numerize.com/",
|
website: "https://numerize.com/",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { Meta, StoryObj } from "@storybook/nextjs"
|
|||||||
import { expect, fn, userEvent, within } from "storybook/test"
|
import { expect, fn, userEvent, within } from "storybook/test"
|
||||||
import { FaCheck } from "react-icons/fa"
|
import { FaCheck } from "react-icons/fa"
|
||||||
|
|
||||||
import type { ButtonLinkProps } from "./Button.tsx"
|
|
||||||
import { Button } from "./Button.tsx"
|
import { Button } from "./Button.tsx"
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
@@ -133,10 +132,10 @@ export const LinkWithIcons: Story = {
|
|||||||
render: (args) => {
|
render: (args) => {
|
||||||
return (
|
return (
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<Button leftIcon={<FaCheck size={18} />} {...(args as ButtonLinkProps)}>
|
<Button leftIcon={<FaCheck size={18} />} {...args}>
|
||||||
Link Left Icon
|
Link Left Icon
|
||||||
</Button>
|
</Button>
|
||||||
<Button rightIcon={<FaCheck size={18} />} {...(args as ButtonLinkProps)}>
|
<Button rightIcon={<FaCheck size={18} />} {...args}>
|
||||||
Link Right Icon
|
Link Right Icon
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { classNames } from "@repo/config-tailwind/classNames"
|
import { classNames } from "@repo/config-tailwind/classNames"
|
||||||
import { Link as NextLink } from "@repo/i18n/routing"
|
import { Link as NextLink } from "@repo/i18n/navigation"
|
||||||
|
|
||||||
import { Spinner } from "../Spinner/Spinner.tsx"
|
import { Spinner } from "../Spinner/Spinner.tsx"
|
||||||
import { Ripple } from "./Ripple.tsx"
|
import { Ripple } from "./Ripple.tsx"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { classNames } from "@repo/config-tailwind/classNames"
|
import { classNames } from "@repo/config-tailwind/classNames"
|
||||||
import { Link as NextLink } from "@repo/i18n/routing"
|
import { Link as NextLink } from "@repo/i18n/navigation"
|
||||||
import { FiExternalLink } from "react-icons/fi"
|
import { FiExternalLink } from "react-icons/fi"
|
||||||
|
|
||||||
export interface LinkProps extends React.ComponentProps<typeof NextLink> {
|
export interface LinkProps extends React.ComponentProps<typeof NextLink> {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useRouter } from "@repo/i18n/routing"
|
import { useRouter } from "@repo/i18n/navigation"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { useEffect, useTransition } from "react"
|
import { useEffect, useTransition } from "react"
|
||||||
import { Button } from "../../Design/Button/Button.tsx"
|
import { Button } from "../../Design/Button/Button.tsx"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { classNames } from "@repo/config-tailwind/classNames"
|
import { classNames } from "@repo/config-tailwind/classNames"
|
||||||
import { usePathname, useRouter } from "@repo/i18n/routing"
|
import { usePathname, useRouter } from "@repo/i18n/navigation"
|
||||||
import { LOCALES } from "@repo/utils/constants"
|
import { LOCALES } from "@repo/utils/constants"
|
||||||
import { useLocale } from "next-intl"
|
import { useLocale } from "next-intl"
|
||||||
import { useEffect, useRef } from "react"
|
import { useEffect, useRef } from "react"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { classNames } from "@repo/config-tailwind/classNames"
|
|||||||
import type { Theme } from "@repo/utils/constants"
|
import type { Theme } from "@repo/utils/constants"
|
||||||
import { THEME_DEFAULT } from "@repo/utils/constants"
|
import { THEME_DEFAULT } from "@repo/utils/constants"
|
||||||
import { ThemeProvider as NextThemeProvider, useTheme as useNextTheme } from "next-themes"
|
import { ThemeProvider as NextThemeProvider, useTheme as useNextTheme } from "next-themes"
|
||||||
import { useEffect, useState } from "react"
|
import { useSyncExternalStore } from "react"
|
||||||
|
|
||||||
export interface ThemeProviderProps extends React.PropsWithChildren {
|
export interface ThemeProviderProps extends React.PropsWithChildren {
|
||||||
forcedTheme?: Theme
|
forcedTheme?: Theme
|
||||||
@@ -30,13 +30,19 @@ export interface UseThemeOutput {
|
|||||||
toggleTheme: () => void
|
toggleTheme: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subscribeToNothing = (): (() => void) => {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
const getClientSnapshot = (): boolean => {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const getServerSnapshot = (): boolean => {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export const useTheme = (): UseThemeOutput => {
|
export const useTheme = (): UseThemeOutput => {
|
||||||
const { setTheme, theme: themeData } = useNextTheme()
|
const { setTheme, theme: themeData } = useNextTheme()
|
||||||
const [isMounted, setIsMounted] = useState(false)
|
const isMounted = useSyncExternalStore(subscribeToNothing, getClientSnapshot, getServerSnapshot)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsMounted(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const theme = isMounted ? (themeData as Theme) : THEME_DEFAULT
|
const theme = isMounted ? (themeData as Theme) : THEME_DEFAULT
|
||||||
|
|
||||||
|
|||||||
Generated
+2818
-2725
File diff suppressed because it is too large
Load Diff
+26
-26
@@ -18,31 +18,31 @@ minimumReleaseAgeIgnoreMissingTime: false
|
|||||||
catalogMode: "strict"
|
catalogMode: "strict"
|
||||||
catalog:
|
catalog:
|
||||||
# TypeScript/Linting
|
# TypeScript/Linting
|
||||||
"turbo": "2.9.16"
|
"turbo": "2.10.11"
|
||||||
"typescript": "6.0.3"
|
"typescript": "7.0.2"
|
||||||
"@types/node": "25.9.1"
|
"@types/node": "26.2.0"
|
||||||
"@total-typescript/ts-reset": "0.6.1"
|
"@total-typescript/ts-reset": "0.6.1"
|
||||||
"oxlint": "1.67.0"
|
"oxlint": "1.79.0"
|
||||||
"oxlint-tsgolint": "0.23.0"
|
"oxlint-tsgolint": "7.0.2001"
|
||||||
"oxfmt": "0.52.0"
|
"oxfmt": "0.64.0"
|
||||||
|
|
||||||
# Utils
|
# Utils
|
||||||
"mime": "4.1.0"
|
"mime": "4.1.0"
|
||||||
|
|
||||||
# React.js/Next.js
|
# React.js/Next.js
|
||||||
"next": "16.2.6"
|
"next": "16.3.2"
|
||||||
"next-intl": "4.13.0"
|
"next-intl": "4.13.7"
|
||||||
"next-themes": "0.4.6"
|
"next-themes": "0.4.6"
|
||||||
"react": "19.2.6"
|
"react": "19.2.8"
|
||||||
"react-dom": "19.2.6"
|
"react-dom": "19.2.8"
|
||||||
"@types/react": "19.2.15"
|
"@types/react": "19.2.18"
|
||||||
"@types/react-dom": "19.2.3"
|
"@types/react-dom": "19.2.5"
|
||||||
"react-icons": "5.6.0"
|
"react-icons": "5.7.0"
|
||||||
|
|
||||||
# Blog
|
# Blog
|
||||||
"@giscus/react": "3.1.0"
|
"@giscus/react": "3.1.0"
|
||||||
"gray-matter": "4.0.3"
|
"gray-matter": "4.0.3"
|
||||||
"katex": "0.17.0"
|
"katex": "0.18.4"
|
||||||
"next-mdx-remote": "5.0.0"
|
"next-mdx-remote": "5.0.0"
|
||||||
"@mdx-js/mdx": "3.1.1"
|
"@mdx-js/mdx": "3.1.1"
|
||||||
"rehype-katex": "7.0.1"
|
"rehype-katex": "7.0.1"
|
||||||
@@ -54,29 +54,29 @@ catalog:
|
|||||||
"@shikijs/rehype": "1.24.0"
|
"@shikijs/rehype": "1.24.0"
|
||||||
|
|
||||||
# Markdown Lint
|
# Markdown Lint
|
||||||
"markdownlint-cli2": "0.22.1"
|
"markdownlint-cli2": "0.23.2"
|
||||||
"markdownlint": "0.40.0"
|
"markdownlint": "0.41.1"
|
||||||
"markdownlint-rule-relative-links": "5.1.0"
|
"markdownlint-rule-relative-links": "5.1.2"
|
||||||
|
|
||||||
# Storybook
|
# Storybook
|
||||||
"storybook": &storybook "10.4.1"
|
"storybook": &storybook "10.5.10"
|
||||||
"@storybook/addon-docs": *storybook
|
"@storybook/addon-docs": *storybook
|
||||||
"@storybook/addon-a11y": *storybook
|
"@storybook/addon-a11y": *storybook
|
||||||
"@storybook/nextjs": *storybook
|
"@storybook/nextjs": *storybook
|
||||||
"@storybook/addon-themes": *storybook
|
"@storybook/addon-themes": *storybook
|
||||||
"@storybook/test-runner": "0.24.4"
|
"@storybook/test-runner": "0.24.4"
|
||||||
"@chromatic-com/storybook": "5.2.1"
|
"@chromatic-com/storybook": "5.3.0"
|
||||||
"chromatic": "16.10.0"
|
"chromatic": "18.5.0"
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
"playwright": &playwright "1.60.0"
|
"playwright": &playwright "1.62.1"
|
||||||
"@playwright/test": *playwright
|
"@playwright/test": *playwright
|
||||||
"start-server-and-test": "3.0.5"
|
"start-server-and-test": "3.0.12"
|
||||||
|
|
||||||
# CSS
|
# CSS
|
||||||
"postcss": "8.5.15"
|
"postcss": "8.5.26"
|
||||||
"tailwindcss": &tailwindcss "4.3.0"
|
"tailwindcss": &tailwindcss "4.3.3"
|
||||||
"@tailwindcss/postcss": *tailwindcss
|
"@tailwindcss/postcss": *tailwindcss
|
||||||
"@tailwindcss/typography": "0.5.19"
|
"@tailwindcss/typography": "0.5.20"
|
||||||
"tailwind-merge": "3.6.0"
|
"tailwind-merge": "3.6.0"
|
||||||
"@fontsource/montserrat": "5.2.8"
|
"@fontsource/montserrat": "5.3.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user