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

Compare commits

...

26 Commits

Author SHA1 Message Date
c094b37bca chore(release): 4.0.0-staging.1 [skip ci] 2024-07-30 22:57:33 +00:00
8dde7f5b42 ci: install playwright --with-deps 2024-07-31 00:47:05 +02:00
cef5ead09f chore: ignore curriculum-vitae 2024-07-30 23:59:33 +02:00
7bde328b96 perf!: monorepo setup + fully static + webp images
BREAKING CHANGE: minimum supported Node.js >= 22.0.0 and pnpm >= 9.5.0
2024-07-30 23:59:06 +02:00
0f44e64c0c chore(release): 3.3.2 [skip ci] 2024-07-20 08:34:00 +00:00
84c192bbef chore: usage of main instead of master 2024-07-20 10:31:14 +02:00
6f78a0686c fix: locale text with font-semibold 2024-07-20 10:28:17 +02:00
2897d181c5 Revert "fix: stop using flag image, use emoji instead"
This reverts commit f64acb68c7.
2024-07-20 10:26:19 +02:00
f94ce7d7bc fix: update Node.js to v20.15.1 (security release) 2024-07-20 10:21:16 +02:00
dd09092842 chore(release): 3.3.1 [skip ci] 2024-07-06 21:01:51 +00:00
f64acb68c7 fix: stop using flag image, use emoji instead 2024-07-06 02:33:49 +02:00
3074945c54 chore(release): 3.3.0 [skip ci] 2024-05-23 20:35:01 +00:00
fc0dfdda5f chore(blog): update shiki to v1.6.0 and update next-mdx-remote to v5.0.0 2024-05-23 22:30:13 +02:00
f62964c62a ci: update GitHub Actions 2024-05-23 21:41:05 +02:00
8ec113c9cb feat(blog): update Git Ultimate Guide to add trick about cherry-pick and diff-commits alias 2024-05-23 10:29:35 +02:00
8a59e9034f chore(release): 3.2.6 [skip ci] 2024-05-21 18:23:28 +00:00
d7121ea833 style: fix tailwindcss linting 2024-05-21 20:18:05 +02:00
c10f690622 build(deps): update dependencies to latest 2024-05-21 20:15:57 +02:00
6915072ab9 chore: delete unused config 2024-05-21 19:31:45 +02:00
dd803bcc51 test: fix should display hello-world blog post 2024-05-21 19:17:29 +02:00
efa33f26ec fix(blog): headings should be aligned with the text, not shifted 2024-05-21 19:06:12 +02:00
5f3dfad988 chore(release): 3.2.5 [skip ci] 2024-05-16 08:09:25 +00:00
b231381cb3 fix: client-side age calculation, more glanular check for isMounted
Allows to render as much as possible on the server side.
While keeping the calculation of the age on the client side to avoid hydratation mismatch.
2024-05-16 10:06:43 +02:00
bbb2e56512 fix: usage of correct heading levels and html tags 2024-05-16 09:56:19 +02:00
66cf6d7438 fix: add scroll behavior: smooth 2024-05-16 09:32:20 +02:00
2a635bf3ba fix: add hover effects 2024-05-16 09:26:05 +02:00
342 changed files with 23026 additions and 25671 deletions

View File

@ -1 +0,0 @@
{ "extends": ["@commitlint/config-conventional"] }

View File

@ -1 +0,0 @@
FROM mcr.microsoft.com/devcontainers/javascript-node:20

View File

@ -1,9 +0,0 @@
services:
workspace:
build:
context: "./"
dockerfile: "./Dockerfile"
volumes:
- "..:/workspace:cached"
command: "sleep infinity"
network_mode: "host"

View File

@ -1,24 +0,0 @@
{
"name": "theoludwig",
"dockerComposeFile": "./compose.yaml",
"service": "workspace",
"workspaceFolder": "/workspace",
"customizations": {
"vscode": {
"settings": {
"remote.autoForwardPorts": false,
"remote.localPortHost": "allInterfaces"
}
},
"extensions": [
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"davidanson.vscode-markdownlint",
"bradlc.vscode-tailwindcss",
"mikestead.dotenv",
"ms-azuretools.vscode-docker"
]
},
"remoteUser": "node"
}

View File

@ -1,7 +1,10 @@
**/.git
**/.turbo
**/.next
**/out
**/dist
**/build
**/storybook-static
**/coverage
**/node_modules
@ -11,10 +14,24 @@
.env.development
secrets
# IDEs and editors
.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
.vscode
# misc
.DS_Store
*.pem
Dockerfile
README.md
# vercel
.vercel
# typescript
*.tsbuildinfo

View File

@ -1,4 +1 @@
COMPOSE_PROJECT_NAME=theoludwig
HOSTNAME=0.0.0.0
PORT=3000
NEXT_TELEMETRY_DISABLED=1
WEBSITE_PORT=3000

View File

@ -1,39 +0,0 @@
{
"root": true,
"extends": [
"conventions",
"next/core-web-vitals",
"plugin:tailwindcss/recommended",
"prettier"
],
"plugins": ["prettier"],
"parserOptions": {
"project": "./tsconfig.json"
},
"settings": {
"tailwindcss": {
"callees": ["classNames"]
},
"react": {
"version": "detect"
}
},
"rules": {
"prettier/prettier": "error",
"react/self-closing-comp": [
"error",
{
"component": true,
"html": true
}
],
"react/void-dom-elements-no-children": "error",
"react/jsx-boolean-value": "error"
},
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"parser": "@typescript-eslint/parser"
}
]
}

View File

@ -1,25 +0,0 @@
name: "Build"
on:
push:
branches: [develop]
pull_request:
branches: [master, develop]
jobs:
build:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.1.1"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
with:
node-version: "20.x"
cache: "npm"
- name: "Install dependencies"
run: "npm clean-install"
- name: "Build"
run: "npm run build"

33
.github/workflows/chromatic.yml vendored Normal file
View File

@ -0,0 +1,33 @@
name: "Chromatic"
on:
push:
branches: [develop]
pull_request:
branches: [develop, staging, main]
jobs:
chromatic:
timeout-minutes: 30
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.1.7"
with:
fetch-depth: 0
- uses: "pnpm/action-setup@v4.0.0"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.3"
with:
node-version: "22.x"
cache: "pnpm"
- name: "Install dependencies"
run: "pnpm install --frozen-lockfile"
- name: "Run Chromatic"
uses: "chromaui/action@latest"
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
workingDir: "apps/storybook"

43
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,43 @@
name: "CI"
on:
push:
branches: [develop]
pull_request:
branches: [develop, staging, main]
jobs:
ci:
timeout-minutes: 30
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.1.7"
- uses: "pnpm/action-setup@v4.0.0"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.3"
with:
node-version: "22.x"
cache: "pnpm"
- name: "Install dependencies"
run: "pnpm install --frozen-lockfile"
- name: "Install Playwright"
run: "pnpm exec playwright install --with-deps"
- run: "node --run lint:editorconfig"
- run: "node --run lint:markdown"
- run: "node --run lint:prettier"
- run: "node --run lint:eslint"
- run: "node --run lint:typescript"
- run: "node --run test"
- run: "node --run build"
commitlint:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.1.7"
- uses: "wagoid/commitlint-github-action@v6.0.1"

View File

@ -1,42 +0,0 @@
name: "Lint"
on:
push:
branches: [develop]
pull_request:
branches: [master, develop]
jobs:
lint:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.1.1"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
with:
node-version: "20.x"
cache: "npm"
- name: "Install dependencies"
run: "npm clean-install"
- name: "lint:commit"
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
- name: "lint:editorconfig"
run: "npm run lint:editorconfig"
- name: "lint:markdown"
run: "npm run lint:markdown"
- name: "lint:eslint"
run: "npm run lint:eslint"
- name: "lint:prettier"
run: "npm run lint:prettier"
- name: "lint:dotenv"
uses: "dotenv-linter/action-dotenv-linter@v2.18.0"
with:
github_token: ${{ secrets.github_token }}

View File

@ -2,13 +2,19 @@ name: "Release"
on:
push:
branches: [master]
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.1.1"
- uses: "actions/checkout@v4.1.7"
with:
fetch-depth: 0
persist-credentials: false
@ -20,17 +26,19 @@ jobs:
git_user_signingkey: true
git_commit_gpgsign: true
- uses: "pnpm/action-setup@v4.0.0"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
uses: "actions/setup-node@v4.0.3"
with:
node-version: "20.x"
cache: "npm"
node-version: "22.x"
cache: "pnpm"
- name: "Install dependencies"
run: "npm clean-install"
run: "pnpm install --frozen-lockfile"
- name: "Release"
run: "npm run release"
run: "node --run release"
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}

View File

@ -1,48 +0,0 @@
name: "Test"
on:
push:
branches: [develop]
pull_request:
branches: [master, develop]
jobs:
test-unit:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.1.1"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
with:
node-version: "20.x"
cache: "npm"
- name: "Install dependencies"
run: "npm clean-install"
- name: "Unit Test"
run: "npm run test:unit"
test-e2e:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4.1.1"
- name: "Setup Node.js"
uses: "actions/setup-node@v4.0.2"
with:
node-version: "20.x"
cache: "npm"
- name: "Install dependencies"
run: "npm clean-install"
- name: "Build"
run: "npm run build"
- name: "html-w3c-validator"
run: "npm run test:html-w3c-validator"
- name: "End To End (e2e) Test"
run: "npm run test:e2e"

59
.gitignore vendored
View File

@ -1,33 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.npm
# next.js
.next
out
# production
build
dist
public/curriculum-vitae
package-lock.json
.pnpm-store
.pnp
.pnp.js
.yarn/install-state.gz
# testing
coverage
cypress/screenshots
cypress/videos
cypress/downloads
# envs
.env
.env.production
# production
.next/
out/
dist/
build/
apps/website/public/curriculum-vitae/
# misc
.DS_Store
*.pem
.turbo
bin/
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Storybook
*storybook.log
storybook-static
# IDEs and editors
/.idea
.idea
.project
.classpath
.c9/
@ -35,17 +41,14 @@ npm-debug.log*
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# local env files
.env
.env.production
.env*.local
# misc
.DS_Store
.lighthouseci
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# next-env.d.ts

View File

@ -1,10 +0,0 @@
image: "gitpod/workspace-full"
tasks:
- before: "cp .env.example .env"
init: "npm clean-install"
command: "npm run dev"
ports:
- port: 3000
onOpen: "open-preview"

View File

@ -1,6 +0,0 @@
{
"$schema": "https://raw.githubusercontent.com/theoludwig/html-w3c-validator/master/schema/html-w3c-validatorrc-schema.json",
"urls": ["http://127.0.0.1:3000/", "http://127.0.0.1:3000/blog"],
"files": ["./public/curriculum-vitae/index.html"],
"severities": ["error"]
}

View File

@ -1,3 +0,0 @@
#!/usr/bin/env sh
npm run lint:commit -- --edit

View File

@ -1,3 +0,0 @@
#!/usr/bin/env sh
npm run lint:staged

View File

@ -1,7 +0,0 @@
{
"**/*": ["editorconfig-checker", "prettier --write --ignore-unknown"],
"**/*.md": ["markdownlint-cli2 --fix --no-globs"],
"**/*.{js,jsx,ts,tsx}": [
"eslint --fix --max-warnings 0 --report-unused-disable-directives"
]
}

2
.npmrc
View File

@ -1 +1 @@
save-exact=true
save-exact = true

11
.prettierrc.json Normal file → Executable file
View File

@ -1,4 +1,13 @@
{
"semi": false,
"plugins": ["prettier-plugin-tailwindcss"]
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindFunctions": ["classNames", "cva"],
"overrides": [
{
"files": "pnpm-lock.yaml",
"options": {
"rangeEnd": 0
}
}
]
}

View File

@ -1,28 +1,22 @@
{
"branches": ["master"],
"branches": ["main", { "name": "staging", "prerelease": true }],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/commit-analyzer",
"@semantic-release/exec",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/npm",
{
"npmPublish": false
"prepareCmd": "replace-in-files --regex='version\": *\"[^\"]*' --replacement='\"version\": \"${nextRelease.version}\"' '**/package.json' '!**/node_modules/**'"
}
],
[
"@semantic-release/git",
{
"assets": ["package.json", "package-lock.json"],
"assets": [
"package.json",
"apps/*/package.json",
"packages/*/package.json"
],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}
],
@ -30,8 +24,10 @@
[
"@saithodev/semantic-release-backmerge",
{
"branches": [{ "from": "master", "to": "develop" }],
"backmergeStrategy": "merge"
"branches": [
{ "from": "main", "to": "develop" },
{ "from": "staging", "to": "develop" }
]
}
]
]

View File

@ -1,5 +1,6 @@
{
"recommendations": [
"Vercel.turbo-vsc",
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",

43
.vscode/react.code-snippets vendored Normal file
View File

@ -0,0 +1,43 @@
{
"React Component": {
"scope": "typescriptreact",
"prefix": "rfc",
"body": [
"export interface ${1:ComponentName}Props {}",
"",
"export const ${1:ComponentName}: React.FC<${1:ComponentName}Props> = () => {",
" return (",
" <div>",
" <h1>${1:ComponentName}</h1>",
" </div>",
" )",
"}",
"",
],
"description": "React Component",
},
"React Component Story": {
"scope": "typescriptreact",
"prefix": "rfcs",
"body": [
"import type { Meta, StoryObj } from \"@storybook/react\"",
"",
"import { ${1:ComponentName} as ${1:ComponentName}Component } from \"./${1:ComponentName}\"",
"",
"const meta = {",
" title: \"${1:ComponentName}\",",
" component: ${1:ComponentName}Component",
"} satisfies Meta<typeof ${1:ComponentName}Component>",
"",
"export default meta",
"",
"type Story = StoryObj<typeof meta>",
"",
"export const ${1:ComponentName}: Story = {",
" args: {}",
"}",
"",
],
"description": "React Component Story",
},
}

10
.vscode/settings.json vendored
View File

@ -1,14 +1,20 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.bracketPairColorization.enabled": true,
"editor.wordWrap": "on",
"prettier.configPath": ".prettierrc.json",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
},
"eslint.options": {
"ignorePath": ".gitignore"
},
"prettier.ignorePath": ".gitignore"
"prettier.ignorePath": ".gitignore",
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}

View File

@ -29,12 +29,10 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
## Getting Started
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/theoludwig/theoludwig)
### Prerequisites
- [Node.js](https://nodejs.org/) >= 20.0.0
- [npm](https://www.npmjs.com/) >= 10.0.0
- [Node.js](https://nodejs.org/) >= 22.0.0
- [pnpm](https://pnpm.io/) >= 9.5.0
### Installation
@ -42,21 +40,35 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
# Clone the repository
git clone git@github.com:theoludwig/theoludwig.git
# Go to the project root
cd theoludwig
# Configure environment variables
cp .env.example .env
cp apps/website/.env.example apps/website/.env
# Install
npm clean-install
# Install dependencies
pnpm install --frozen-lockfile
# Install Playwright browser binaries and their dependencies (tests)
pnpm exec playwright install --with-deps
```
### Local Development environment
### Development
```sh
# Run website
npm run dev
# Start the development server
node --run dev
# Lint
node --run lint:editorconfig
node --run lint:markdown
node --run lint:prettier
node --run lint:eslint
node --run lint:typescript
# Tests
node --run test
# Build
node --run build
```
### Production environment with [Docker](https://www.docker.com/)
@ -66,6 +78,6 @@ npm run dev
docker compose up --build
```
### Services started
#### Services started
- `website`: <http://127.0.0.1:3000>
`theoludwig`: <http://127.0.0.1:3000>

View File

@ -1,28 +0,0 @@
FROM node:20.12.2 AS builder-dependencies
WORKDIR /usr/src/application
COPY ./package*.json ./
RUN npm clean-install
FROM node:20.12.2 AS builder
ENV NEXT_TELEMETRY_DISABLED=1
ENV IS_STANDALONE=true
WORKDIR /usr/src/application
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
COPY ./ ./
RUN npm run build
FROM node:20.12.2-slim AS runner
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV NEXT_TELEMETRY_DISABLED=1
ENV IS_STANDALONE=true
WORKDIR /usr/src/application
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 applicationrunner
USER applicationrunner
COPY --from=builder-dependencies --chown=applicationrunner:nodejs /usr/src/application/node_modules ./node_modules
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/.next/standalone ./
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/.next/static ./.next/static
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/public ./public
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/i18n/translations ./i18n/translations
COPY --from=builder --chown=applicationrunner:nodejs /usr/src/application/next.config.js ./next.config.js
CMD ["./server.js"]

View File

@ -8,7 +8,7 @@
<a href="https://github.com/theoludwig"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a>
<a href="https://gitlab.com/theoludwig"><img alt="GitLab" src="https://img.shields.io/badge/-GitLab-303030?style=flat&labelColor=303030&logo=gitlab&logoColor=white"/></a>
<a href="https://www.npmjs.com/~theoludwig"><img alt="npm" src="https://img.shields.io/badge/-npm-c4302b?style=flat&labelColor=c4302b&logo=npm&logoColor=white"/></a>
<a href="https://twitter.com/theoludwig_"><img alt="Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=twitter&logoColor=white"/></a>
<a href="https://twitter.com/theoludwig_"><img alt="Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=x&logoColor=white"/></a>
<a href="https://www.youtube.com/@theo_ludwig"><img alt="YouTube" src="https://img.shields.io/badge/-YouTube-c4302b?style=flat&labelColor=c4302b&logo=youtube&logoColor=white"/></a>
<a href="https://www.twitch.tv/theoludwig"><img alt="Twitch" src="https://img.shields.io/badge/-Twitch-9147FF?style=flat&labelColor=9147FF&logo=twitch&logoColor=white"/></a>
<a href="https://theoludwig.fr/"><img alt="Website" src="https://img.shields.io/badge/-Website-181818?style=flat&labelColor=181818&logo=Google-Chrome&logoColor=white"/></a>
@ -23,7 +23,7 @@
{
"name": "Théo LUDWIG",
"pronouns": "He/Him",
"birthDate": "31/03/2003",
"birthDate": "2003-03-31",
"nationality": "Alsace, France",
"interests": ["Developer Full Stack", "Open-Source Enthusiast"],
"skills": {

View File

@ -1,11 +0,0 @@
import { Loader } from "@/components/design/Loader"
const Loading = (): JSX.Element => {
return (
<main className="flex flex-1 flex-col items-center justify-center">
<Loader />
</main>
)
}
export default Loading

View File

@ -1,11 +0,0 @@
import { Loader } from "@/components/design/Loader"
const Loading = (): JSX.Element => {
return (
<main className="flex flex-1 flex-col items-center justify-center">
<Loader />
</main>
)
}
export default Loading

View File

@ -1,42 +0,0 @@
import { Suspense } from "react"
import type { Metadata } from "next"
import { BlogPosts } from "@/blog/BlogPosts"
import { Loader } from "@/components/design/Loader"
const title = "Blog | Théo LUDWIG"
const description =
"The latest news about my journey of learning computer science."
export const metadata: Metadata = {
title,
description,
openGraph: {
title,
description,
},
twitter: {
title,
description,
},
}
const BlogPage = async (): Promise<JSX.Element> => {
return (
<main className="flex flex-1 flex-col flex-wrap items-center">
<div className="mt-10 flex flex-col items-center">
<h1 className="text-4xl font-semibold text-primary dark:text-primary-dark">
Blog
</h1>
<p className="mt-6 text-center" data-cy="blog-post-date">
{description}
</p>
</div>
<Suspense fallback={<Loader className="mt-8" />}>
<BlogPosts />
</Suspense>
</main>
)
}
export default BlogPage

View File

@ -1,32 +0,0 @@
"use client"
import { useEffect } from "react"
export interface ErrorHandlingProps {
error: Error
}
const ErrorHandling = (props: ErrorHandlingProps): JSX.Element => {
const { error } = props
useEffect(() => {
console.error(error)
}, [error])
return (
<main className="flex flex-1 flex-col items-center justify-center">
<h1 className="my-6 text-4xl font-semibold">
Error{" "}
<span
className="text-primary dark:text-primary-dark"
data-cy="status-code"
>
500
</span>
</h1>
<p className="text-center text-lg">Server error</p>
</main>
)
}
export default ErrorHandling

View File

@ -1,78 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.break-wrap-words {
word-wrap: break-word;
word-break: break-word;
}
.text-base {
line-height: 1.75rem;
}
.prose {
@apply !max-w-5xl scroll-smooth text-gray dark:text-gray-300;
}
.prose p {
@apply text-justify;
}
.prose [id]::before {
content: "";
display: block;
height: 90px;
margin-top: -90px;
visibility: hidden;
}
.prose a,
.prose strong {
@apply !font-semibold text-primary dark:text-primary-dark;
}
strong,
b {
@apply font-bold;
}
.prose h2,
.prose h3,
.prose h4,
.prose h5,
.prose h6 {
@apply mt-1 text-gray dark:text-gray-dark;
}
.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;
}
code {
counter-reset: step;
counter-increment: step 0;
}
code .line::before {
content: counter(step);
counter-increment: step;
width: 1rem;
margin-right: 1.5rem;
display: inline-block;
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;
}

View File

@ -1,77 +0,0 @@
import type { Metadata } from "next"
import classNames from "clsx"
import "@fontsource/montserrat/400.css"
import "@fontsource/montserrat/600.css"
import "./globals.css"
import { Header } from "@/components/Header"
import { Footer } from "@/components/Footer"
import { getI18n } from "@/i18n/i18n.server"
import { getTheme } from "@/theme/theme.server"
const title = "Théo LUDWIG"
const description =
"Théo LUDWIG - Developer Full Stack • Open-Source Enthusiast"
const image = "/images/logo.png"
const url = new URL("https://theoludwig.fr")
const locale = "fr-FR, en-US"
export const metadata: Metadata = {
title,
description,
metadataBase: url,
openGraph: {
title,
description,
url,
siteName: title,
images: [
{
url: image,
width: 96,
height: 96,
},
],
locale,
type: "website",
},
twitter: {
card: "summary",
title,
description,
images: [image],
},
}
interface RootLayoutProps {
children: React.ReactNode
}
const RootLayout = (props: RootLayoutProps): JSX.Element => {
const { children } = props
const i18n = getI18n()
const theme = getTheme()
return (
<html
lang={i18n.locale}
className={classNames({
dark: theme === "dark",
light: theme === "light",
})}
style={{
colorScheme: theme,
}}
>
<body className="flex min-h-screen flex-col bg-white font-headline text-black dark:bg-black dark:text-white">
<Header />
{children}
<Footer />
</body>
</html>
)
}
export default RootLayout

View File

@ -1,11 +0,0 @@
import { Loader } from "@/components/design/Loader"
const Loading = (): JSX.Element => {
return (
<main className="flex flex-1 flex-col items-center justify-center">
<Loader />
</main>
)
}
export default Loading

View File

@ -1,32 +0,0 @@
import Link from "next/link"
import { getI18n } from "@/i18n/i18n.server"
const NotFound = (): JSX.Element => {
const i18n = getI18n()
return (
<main className="flex flex-1 flex-col items-center justify-center">
<h1 className="my-6 text-4xl font-semibold">
{i18n.translate("errors.error")}{" "}
<span
className="text-primary dark:text-primary-dark"
data-cy="status-code"
>
404
</span>
</h1>
<p className="text-center text-lg">
{i18n.translate("errors.not-found")}{" "}
<Link
href="/"
className="text-primary hover:underline dark:text-primary-dark"
>
{i18n.translate("errors.return-to-home-page")}
</Link>
</p>
</main>
)
}
export default NotFound

View File

@ -1,59 +0,0 @@
import { RevealFade } from "@/components/design/RevealFade"
import { Section } from "@/components/design/Section"
import { Interests } from "@/components/Interests"
import { Portfolio } from "@/components/Portfolio"
import { Profile } from "@/components/Profile"
import { SocialMediaList } from "@/components/Profile/SocialMediaList"
import { Skills } from "@/components/Skills"
import { OpenSource } from "@/components/OpenSource"
import { getI18n } from "@/i18n/i18n.server"
const HomePage = (): JSX.Element => {
const i18n = getI18n()
return (
<main className="flex flex-col md:mx-auto md:max-w-4xl lg:max-w-7xl">
<Section isMain id="about">
<Profile />
<SocialMediaList />
</Section>
<RevealFade>
<Section
id="interests"
heading={i18n.translate("home.interests.title")}
>
<Interests />
</Section>
</RevealFade>
<RevealFade>
<Section
id="skills"
heading={i18n.translate("home.skills.title")}
withoutShadowContainer
>
<Skills />
</Section>
</RevealFade>
<RevealFade>
<Section
id="portfolio"
heading={i18n.translate("home.portfolio.title")}
withoutShadowContainer
>
<Portfolio />
</Section>
</RevealFade>
<RevealFade>
<Section id="open-source" heading="Open source" withoutShadowContainer>
<OpenSource />
</Section>
</RevealFade>
</main>
)
}
export default HomePage

View File

@ -0,0 +1,4 @@
{
"root": true,
"extends": ["@repo/eslint-config"]
}

View File

@ -0,0 +1,34 @@
import type { StorybookConfig } from "@storybook/nextjs"
const config: StorybookConfig = {
core: {
disableTelemetry: true,
},
docs: {
defaultName: "Documentation",
},
stories: ["../../../packages/**/*.stories.tsx", "../stories/*.mdx"],
addons: [
"@storybook/addon-essentials",
"@storybook/addon-storysource",
"@storybook/addon-a11y",
"@storybook/addon-links",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
"storybook-dark-mode",
],
framework: {
name: "@storybook/nextjs",
options: {},
},
features: {
experimentalRSC: true,
},
typescript: {
check: false,
reactDocgen: "react-docgen-typescript",
},
staticDirs: ["../../website/public"],
}
export default config

View File

@ -0,0 +1,52 @@
import "@repo/config-tailwind/styles.css"
import { defaultTranslationValues, Locale } from "@repo/i18n/config"
import i18nMessagesEnglish from "@repo/i18n/translations/en-US.json"
import type { Preview } from "@storybook/react"
import { NextIntlClientProvider } from "next-intl"
import { ThemeProvider as NextThemeProvider } from "next-themes"
import React from "react"
const preview: Preview = {
parameters: {
nextjs: {
appDirectory: true,
},
options: {
storySort: {
order: ["Design System", "User Interface", "Errors", "Feature"],
},
},
backgrounds: { disable: true },
darkMode: {
darkClass: "dark",
lightClass: "light",
classTarget: "html",
stylePreview: true,
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
decorators: [
(Story) => {
const locale = "en-US" satisfies Locale
return (
<NextThemeProvider enableColorScheme={false}>
<NextIntlClientProvider
messages={i18nMessagesEnglish}
locale={locale}
defaultTranslationValues={defaultTranslationValues}
>
<Story />
</NextIntlClientProvider>
</NextThemeProvider>
)
},
],
}
export default preview

View File

@ -0,0 +1,35 @@
import type { TestRunnerConfig } from "@storybook/test-runner"
import { getStoryContext } from "@storybook/test-runner"
import { checkA11y, configureAxe, injectAxe } from "axe-playwright"
/*
* See https://storybook.js.org/docs/writing-tests/test-runner#test-hook-api
* to learn more about the test-runner hooks API.
*/
const config: TestRunnerConfig = {
async preVisit(page) {
await injectAxe(page)
},
async postVisit(page, context) {
const storyContext = await getStoryContext(page, context)
if (storyContext.parameters?.a11y?.disable) {
return
}
await configureAxe(page, {
rules: storyContext.parameters?.a11y?.config?.rules,
})
await checkA11y(page, "#storybook-root", {
verbose: false,
detailedReport: true,
detailedReportOptions: {
html: true,
},
})
},
}
export default config

View File

@ -0,0 +1,7 @@
{
"projectId": "Project:66a7a85ea85df74afbec7682",
"buildScriptName": "build",
"storybookBaseDir": "apps/storybook",
"onlyChanged": true,
"zip": true
}

View File

@ -0,0 +1,55 @@
{
"name": "@repo/storybook",
"version": "4.0.0-staging.1",
"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 \"dev\" http://127.0.0.1:6006 \"test:storybook\"",
"test:storybook": "test-storybook",
"test:storybook-coverage": "test-storybook --coverage",
"chromatic": "chromatic"
},
"dependencies": {
"@repo/config-tailwind": "workspace:*",
"@repo/i18n": "workspace:*",
"@repo/ui": "workspace:*",
"@repo/blog": "workspace:*",
"next": "catalog:",
"next-intl": "catalog:",
"next-themes": "catalog:",
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@chromatic-com/storybook": "catalog:",
"@playwright/test": "catalog:",
"@storybook/addon-a11y": "catalog:",
"@storybook/addon-essentials": "catalog:",
"@storybook/addon-interactions": "catalog:",
"@storybook/addon-links": "catalog:",
"@storybook/addon-storysource": "catalog:",
"@storybook/addon-themes": "catalog:",
"@storybook/blocks": "catalog:",
"@storybook/nextjs": "catalog:",
"@storybook/react": "catalog:",
"@storybook/test": "catalog:",
"@storybook/test-runner": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"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:",
"typescript": "catalog:"
}
}

View File

@ -1,6 +1,7 @@
module.exports = {
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
export default config

View File

@ -0,0 +1,31 @@
import { Meta, Title, ColorPalette, ColorItem } from "@storybook/blocks"
import tailwindConfig from "@repo/config-tailwind"
<Meta title="Design System/Colors" />
<Title>Colors</Title>
<ColorPalette>
{Object.entries(tailwindConfig.theme.colors).map(
([colorName, colorValue]) => {
const colors = {}
if (typeof colorValue === "string") {
colors[colorName] = colorValue
} else {
colors.light = colorValue.DEFAULT
colors.dark = colorValue.dark
}
return (
<ColorItem
key={colorName}
title={colorName}
colors={colors}
/>
)
}
)}
</ColorPalette>

View File

@ -0,0 +1,9 @@
import sharedConfig from "@repo/config-tailwind"
/** @type {Pick<import('tailwindcss').Config, "presets" | "content">} */
const config = {
content: [".storybook/preview.tsx", "../../packages/**/*.tsx"],
presets: [sharedConfig],
}
export default config

View File

@ -0,0 +1,3 @@
HOSTNAME=0.0.0.0
PORT=3000
NEXT_TELEMETRY_DISABLED=1

View File

@ -0,0 +1,15 @@
{
"root": true,
"extends": ["@repo/eslint-config/nextjs/.eslintrc.json"],
"ignorePatterns": ["public/"],
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"plugins": ["@typescript-eslint"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": true
}
}
]
}

36
apps/website/Dockerfile Normal file
View File

@ -0,0 +1,36 @@
FROM node:22.4.1-slim AS node-pnpm
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /usr/src/app
FROM node-pnpm AS builder
RUN pnpm install --global turbo@2.0.9
COPY ./ ./
RUN turbo prune @repo/website --docker
FROM node-pnpm AS installer
ENV IS_STANDALONE=true
COPY .gitignore .gitignore
COPY --from=builder /usr/src/app/out/json/ ./
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
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
USER applicationrunner
COPY --from=installer /usr/src/app/apps/website/next.config.js ./
COPY --from=installer /usr/src/app/apps/website/package.json ./
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/.next/standalone ./
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/.next/static ./apps/website/.next/static
COPY --from=installer --chown=applicationrunner:nodejs /usr/src/app/apps/website/public ./apps/website/public
CMD ["node", "apps/website/server.js"]

View File

@ -0,0 +1,7 @@
import { notFound } from "next/navigation"
const CatchAllPage: React.FC = () => {
return notFound()
}
export default CatchAllPage

View File

@ -1,10 +1,8 @@
import type { Metadata } from "next"
import { notFound } from "next/navigation"
import "katex/dist/katex.min.css"
import { getBlogPostBySlug } from "@/blog/blog"
import { BlogPost } from "@/blog/BlogPost"
import { getBlogPostBySlug, getBlogPosts } from "@repo/blog"
import { BlogPostUI } from "@repo/blog/BlogPostUI"
interface BlogPostPageProps {
params: {
@ -35,10 +33,26 @@ export const generateMetadata = async (
}
}
const BlogPostPage = async (props: BlogPostPageProps): Promise<JSX.Element> => {
export const generateStaticParams = async (): Promise<
Array<{ slug: string }>
> => {
const posts = await getBlogPosts()
return posts.map((post) => {
return {
slug: post.slug,
}
})
}
const BlogPostPage: React.FC<BlogPostPageProps> = async (props) => {
const { params } = props
return <BlogPost slug={params.slug} />
const blogPost = await getBlogPostBySlug(params.slug)
if (blogPost == null) {
return notFound()
}
return <BlogPostUI blogPost={blogPost} />
}
export default BlogPostPage

View File

@ -0,0 +1,55 @@
import { getBlogPosts } from "@repo/blog"
import { BlogPosts } from "@repo/blog/BlogPosts"
import { LOCALE_DEFAULT, type LocaleProps } from "@repo/i18n/config"
import {
Section,
SectionDescription,
SectionTitle,
} from "@repo/ui/design/Section"
import { MainLayout } from "@repo/ui/MainLayout"
import type { Metadata } from "next"
import { unstable_setRequestLocale } from "next-intl/server"
const title = "Blog | Théo LUDWIG"
const description =
"The latest news about my journey of learning computer science."
export const generateMetadata = async (): Promise<Metadata> => {
return {
title,
description,
openGraph: {
title,
description,
locale: LOCALE_DEFAULT,
},
twitter: {
title,
description,
},
}
}
interface BlogPageProps extends LocaleProps {}
const BlogPage: React.FC<BlogPageProps> = async (props) => {
const { params } = props
// Enable static rendering
unstable_setRequestLocale(params.locale)
const posts = await getBlogPosts()
return (
<MainLayout>
<Section verticalSpacing horizontalSpacing>
<SectionTitle>Blog</SectionTitle>
<SectionDescription>{description}</SectionDescription>
<BlogPosts posts={posts} />
</Section>
</MainLayout>
)
}
export default BlogPage

View File

@ -0,0 +1,10 @@
"use client"
import type { ErrorServerProps } from "@repo/ui/Errors/ErrorServer"
import { ErrorServer } from "@repo/ui/Errors/ErrorServer"
const ErrorBoundaryPage: React.FC<ErrorServerProps> = (props) => {
return <ErrorServer {...props} />
}
export default ErrorBoundaryPage

View File

@ -0,0 +1,91 @@
import "@repo/config-tailwind/styles.css"
import type { Locale, LocaleProps } from "@repo/i18n/config"
import { LOCALES } from "@repo/i18n/config"
import { Footer } from "@repo/ui/Footer"
import { Header } from "@repo/ui/Header"
import { ThemeProvider } from "@repo/ui/Header/SwitchTheme"
import { VERSION } from "@repo/utils/constants"
import type { Metadata } from "next"
import { NextIntlClientProvider } from "next-intl"
import {
getMessages,
getTranslations,
unstable_setRequestLocale,
} from "next-intl/server"
export const generateMetadata = async ({
params,
}: LocaleProps): Promise<Metadata> => {
const t = await getTranslations({ locale: params.locale })
const title = t("meta.title")
const description = `${title} - ${t("meta.description")}`
const image = "/images/logo.webp"
const url = new URL("https://theoludwig.fr")
const locale = LOCALES.join(", ")
return {
title,
description,
metadataBase: url,
openGraph: {
title,
description,
url,
siteName: title,
images: [
{
url: image,
width: 96,
height: 96,
},
],
locale,
type: "website",
},
twitter: {
card: "summary",
title,
description,
images: [image],
},
}
}
export const generateStaticParams = (): Array<{ locale: Locale }> => {
return LOCALES.map((locale) => {
return {
locale,
}
})
}
interface LocaleLayoutProps extends React.PropsWithChildren {
params: {
locale: Locale
}
}
const LocaleLayout: React.FC<LocaleLayoutProps> = async (props) => {
const { children, params } = props
// Enable static rendering
unstable_setRequestLocale(params.locale)
const messages = await getMessages()
return (
<html lang={params.locale} suppressHydrationWarning>
<body>
<ThemeProvider>
<NextIntlClientProvider messages={messages}>
<Header />
{children}
<Footer version={VERSION} />
</NextIntlClientProvider>
</ThemeProvider>
</body>
</html>
)
}
export default LocaleLayout

View File

@ -0,0 +1,12 @@
import { MainLayout } from "@repo/ui/MainLayout"
import { Spinner } from "@repo/ui/design/Spinner"
const Loading: React.FC = () => {
return (
<MainLayout center>
<Spinner size={50} />
</MainLayout>
)
}
export default Loading

View File

@ -0,0 +1,10 @@
import { ErrorNotFound } from "@repo/ui/Errors/ErrorNotFound"
/**
* Note that `app/[locale]/[...rest]/page.tsx` is necessary for this page to render.
*/
const NotFound: React.FC = () => {
return <ErrorNotFound />
}
export default NotFound

View File

@ -0,0 +1,42 @@
import type { LocaleProps } from "@repo/i18n/config"
import { About } from "@repo/ui/About"
import { RevealFade } from "@repo/ui/design/Section"
import { Interests } from "@repo/ui/Interests"
import { MainLayout } from "@repo/ui/MainLayout"
import { OpenSource } from "@repo/ui/OpenSource"
import { Portfolio } from "@repo/ui/Portfolio"
import { Skills } from "@repo/ui/Skills"
import { unstable_setRequestLocale } from "next-intl/server"
interface HomePageProps extends LocaleProps {}
const HomePage: React.FC<HomePageProps> = (props) => {
const { params } = props
// Enable static rendering
unstable_setRequestLocale(params.locale)
return (
<MainLayout>
<About />
<RevealFade>
<Interests />
</RevealFade>
<RevealFade>
<Skills />
</RevealFade>
<RevealFade>
<Portfolio />
</RevealFade>
<RevealFade>
<OpenSource />
</RevealFade>
</MainLayout>
)
}
export default HomePage

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,7 @@
interface RootLayoutProps extends React.PropsWithChildren {}
const RootLayout = ({ children }: RootLayoutProps): React.ReactNode => {
return children
}
export default RootLayout

View File

@ -0,0 +1,20 @@
"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

3
apps/website/i18n.ts Normal file
View File

@ -0,0 +1,3 @@
import i18nConfig from "@repo/i18n/i18n"
export default i18nConfig

View File

@ -0,0 +1,23 @@
import { LOCALES, LOCALE_DEFAULT, LOCALE_PREFIX } from "@repo/i18n/config"
import createMiddleware from "next-intl/middleware"
export default createMiddleware({
locales: LOCALES,
defaultLocale: LOCALE_DEFAULT,
localePrefix: LOCALE_PREFIX,
})
export const config = {
matcher: [
// Enable a redirect to a matching locale at the root
"/",
// Set a cookie to remember the previous locale for
// all requests that have a locale prefix
"/(en-US|fr-FR)/:path*",
// Enable redirects that add missing locales
// (e.g. `/pathnames` -> `/en/pathnames`)
"/((?!_next|_vercel|.*\\..*).*)",
],
}

5
apps/website/next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -0,0 +1,22 @@
import createNextIntlPlugin from "next-intl/plugin"
const IS_STANDALONE = process.env.IS_STANDALONE === "true"
/** @type {import('next').NextConfig} */
const nextConfig = {
output: IS_STANDALONE ? "standalone" : undefined,
// https://github.com/hashicorp/next-mdx-remote/issues/436#issuecomment-2066971842
transpilePackages: ["next-mdx-remote", "shiki"],
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
}
const withNextIntl = createNextIntlPlugin()
export default withNextIntl(nextConfig)

40
apps/website/package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "@repo/website",
"version": "4.0.0-staging.1",
"private": true,
"type": "module",
"imports": {
"#*": "./*"
},
"scripts": {
"dev": "next dev --port 3000 --turbo",
"build": "next build",
"start": "next start --port 3000",
"lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives",
"lint:typescript": "tsc --noEmit"
},
"dependencies": {
"@repo/blog": "workspace:*",
"@repo/config-tailwind": "workspace:*",
"@repo/utils": "workspace:*",
"@repo/i18n": "workspace:*",
"@repo/ui": "workspace:*",
"next": "catalog:",
"next-intl": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"sharp": "catalog:"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/config-typescript": "workspace:*",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@total-typescript/ts-reset": "catalog:",
"eslint": "catalog:",
"postcss": "catalog:",
"tailwindcss": "catalog:",
"typescript": "catalog:"
}
}

View File

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

View File

@ -0,0 +1 @@
/*! modern-normalize v2.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */*,:before,:after{box-sizing:border-box}html{font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";line-height:1.15;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4}body{margin:0}hr{height:0;color:inherit}abbr[title]{text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}body{font-family:Montserrat,Arial,"sans-serif";background:#f0f0f0;color:#333;line-height:1.42857143;font-size:14px}hr{margin-top:15px;margin-bottom:15px;border:0;border-top:1px solid #eee}p{margin:0}strong{font-weight:600}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}.link-disguise,.link-disguise:hover{color:inherit}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.clear-margin{margin:0}.relative{position:relative}.center-block{display:block;margin-right:auto;margin-left:auto}.text-center{text-align:center}.text-muted{color:#777}.text-uppercase{text-transform:uppercase}.list-unstyled{padding-left:0;list-style:none}.main{padding:5px}.title{font-weight:600}.profile-card-wrapper{position:relative}.card-wrapper{float:none!important;padding:5px}.profile-card-wrapper .profile-card{padding:10px}.card{background:#fff;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}.contact-details{display:flex;justify-content:center}.contact-details .detail{position:relative;min-height:1px;padding:10px}.social-links{line-height:2.5}.experience-description{margin-top:10px}.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;border-width:1px 0 0 0}.card-skills{position:relative}.labels{line-height:2}.space-top{margin-top:10px}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:600;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label-keyword{display:inline-block;font-size:.9em;padding:5px;border:1px solid #357ebd;margin-right:5px}.label-keyword p{margin:0}.section-separated{display:flex}

View File

@ -0,0 +1 @@
(function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))r(e);new MutationObserver(e=>{for(const t of e)if(t.type==="childList")for(const c of t.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&r(c)}).observe(document,{childList:!0,subtree:!0});function s(e){const t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?t.credentials="include":e.crossOrigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function r(e){if(e.ep)return;e.ep=!0;const t=s(e);fetch(e.href,t)}})();const i="31",l="03",u="2003",d=`${u}-${l}-${i}`,f=new Date(d),a=n=>{const o=new Date;let s=o.getFullYear()-n.getFullYear();const r=o.getMonth()-n.getMonth();return(r<0||r===0&&o.getDate()<n.getDate())&&s--,s},g=document.getElementById("year-old");g.textContent=a(f).toString();

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Some files were not shown because too many files have changed in this diff Show More