Compare commits
40 Commits
Author | SHA1 | Date | |
---|---|---|---|
f6bfc466de
|
|||
e4cf714d95
|
|||
d3c86b2a26
|
|||
d2578abeec
|
|||
e51e3bdc19
|
|||
56520830e9
|
|||
2e0138194c
|
|||
4b2e7bae90
|
|||
caa6a90418
|
|||
e82db952db
|
|||
6b29ce9b15
|
|||
5640f1b434
|
|||
6d0dcb50a7
|
|||
70603f1444
|
|||
f42fdbfd0c
|
|||
6a3f335f9f
|
|||
f1509d0af1
|
|||
49599d25ed
|
|||
65e0f4f8b6
|
|||
8d60c2d53a
|
|||
0bbebeab99
|
|||
643e0e5821
|
|||
872b018673
|
|||
2644cb0fb5
|
|||
bc719578d2
|
|||
117c41b1c3
|
|||
b92704b77d
|
|||
bab7581283
|
|||
988fceb2aa
|
|||
5211ba1489
|
|||
6886480cef
|
|||
d78e50638e
|
|||
3b76195d71
|
|||
2dc63ba933
|
|||
336f067c52
|
|||
5fd7f77b6d
|
|||
db0c708c04
|
|||
9d44671fed
|
|||
7bcc5f972c | |||
61172d59e3 |
@ -1 +1 @@
|
||||
FROM mcr.microsoft.com/devcontainers/javascript-node:18
|
||||
FROM mcr.microsoft.com/devcontainers/javascript-node:20
|
||||
|
@ -6,3 +6,4 @@ services:
|
||||
volumes:
|
||||
- '..:/workspace:cached'
|
||||
command: 'sleep infinity'
|
||||
network_mode: 'host'
|
@ -1,25 +1,24 @@
|
||||
{
|
||||
"name": "Divlo",
|
||||
"dockerComposeFile": "./docker-compose.yml",
|
||||
"name": "theoludwig",
|
||||
"dockerComposeFile": "./compose.yaml",
|
||||
"service": "workspace",
|
||||
"workspaceFolder": "/workspace",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"remote.autoForwardPorts": false
|
||||
"remote.autoForwardPorts": false,
|
||||
"remote.localPortHost": "allInterfaces"
|
||||
}
|
||||
},
|
||||
"extensions": [
|
||||
"editorconfig.editorconfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"mikestead.dotenv",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"ms-azuretools.vscode-docker"
|
||||
]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [3000],
|
||||
"postAttachCommand": ["npm", "install"],
|
||||
"remoteUser": "node"
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
.*
|
||||
!.npmrc
|
||||
build
|
||||
.next
|
||||
coverage
|
||||
node_modules
|
||||
|
@ -1,2 +1,2 @@
|
||||
COMPOSE_PROJECT_NAME=divlo
|
||||
COMPOSE_PROJECT_NAME=theoludwig
|
||||
PORT=3000
|
||||
|
@ -1,10 +1,16 @@
|
||||
{
|
||||
"extends": ["conventions", "next/core-web-vitals", "prettier"],
|
||||
"plugins": ["prettier", "unicorn"],
|
||||
"plugins": ["prettier"],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
"prettier/prettier": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"parser": "@typescript-eslint/parser"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
27
.github/workflows/analyze.yml
vendored
@ -1,27 +0,0 @@
|
||||
name: 'Analyze'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
pull_request:
|
||||
branches: [master, develop]
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: 'ubuntu-latest'
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ['javascript']
|
||||
|
||||
steps:
|
||||
- uses: 'actions/checkout@v3.5.2'
|
||||
|
||||
- name: 'Initialize CodeQL'
|
||||
uses: 'github/codeql-action/init@v2'
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: 'Perform CodeQL Analysis'
|
||||
uses: 'github/codeql-action/analyze@v2'
|
6
.github/workflows/build.yml
vendored
@ -10,12 +10,12 @@ jobs:
|
||||
build:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v3.5.2'
|
||||
- uses: 'actions/checkout@v3.5.3'
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@v3.6.0'
|
||||
uses: 'actions/setup-node@v3.7.0'
|
||||
with:
|
||||
node-version: '18.x'
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
|
6
.github/workflows/lint.yml
vendored
@ -10,12 +10,12 @@ jobs:
|
||||
lint:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v3.5.2'
|
||||
- uses: 'actions/checkout@v3.5.3'
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@v3.6.0'
|
||||
uses: 'actions/setup-node@v3.7.0'
|
||||
with:
|
||||
node-version: '18.x'
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
|
13
.github/workflows/release.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
||||
release:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v3.5.2'
|
||||
- uses: 'actions/checkout@v3.5.3'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@ -21,9 +21,9 @@ jobs:
|
||||
git_commit_gpgsign: true
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@v3.6.0'
|
||||
uses: 'actions/setup-node@v3.7.0'
|
||||
with:
|
||||
node-version: '18.x'
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
@ -35,10 +35,3 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}
|
||||
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_EMAIL }}
|
||||
|
||||
- name: 'Deploy to Vercel'
|
||||
run: 'npm run deploy -- --token="${VERCEL_TOKEN}" --prod'
|
||||
env:
|
||||
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
|
12
.github/workflows/test.yml
vendored
@ -10,12 +10,12 @@ jobs:
|
||||
test-unit:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v3.5.2'
|
||||
- uses: 'actions/checkout@v3.5.3'
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@v3.6.0'
|
||||
uses: 'actions/setup-node@v3.7.0'
|
||||
with:
|
||||
node-version: '18.x'
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
@ -27,12 +27,12 @@ jobs:
|
||||
test-e2e:
|
||||
runs-on: 'ubuntu-latest'
|
||||
steps:
|
||||
- uses: 'actions/checkout@v3.5.0'
|
||||
- uses: 'actions/checkout@v3.5.3'
|
||||
|
||||
- name: 'Setup Node.js'
|
||||
uses: 'actions/setup-node@v3.6.0'
|
||||
uses: 'actions/setup-node@v3.7.0'
|
||||
with:
|
||||
node-version: '18.x'
|
||||
node-version: '20.x'
|
||||
cache: 'npm'
|
||||
|
||||
- name: 'Install dependencies'
|
||||
|
4
.gitignore
vendored
@ -12,9 +12,6 @@ out
|
||||
build
|
||||
dist
|
||||
public/curriculum-vitae
|
||||
# PWA
|
||||
public/workbox-*.js
|
||||
public/sw.js
|
||||
|
||||
# testing
|
||||
coverage
|
||||
@ -48,7 +45,6 @@ npm-debug.log*
|
||||
# misc
|
||||
.DS_Store
|
||||
.lighthouseci
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
@ -2,7 +2,7 @@ image: 'gitpod/workspace-full'
|
||||
|
||||
tasks:
|
||||
- before: 'cp .env.example .env'
|
||||
init: 'npm install'
|
||||
init: 'npm clean-install'
|
||||
command: 'npm run dev'
|
||||
|
||||
ports:
|
||||
|
@ -1,8 +1,4 @@
|
||||
{
|
||||
"urls": [
|
||||
"http://127.0.0.1:3000/",
|
||||
"http://127.0.0.1:3000/blog",
|
||||
"http://127.0.0.1:3000/blog/hello-world"
|
||||
],
|
||||
"urls": ["http://127.0.0.1:3000/", "http://127.0.0.1:3000/blog"],
|
||||
"files": ["./public/curriculum-vitae/index.html"]
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ representative at an online or offline event.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
<contact@divlo.fr>.
|
||||
<contact@theoludwig.fr>.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
@ -1,10 +1,10 @@
|
||||
# 💡 Contributing
|
||||
|
||||
Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
|
||||
Thanks a lot for your interest in contributing to **theoludwig.fr**! 🎉
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
**divlo.fr** adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
|
||||
**theoludwig.fr** adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
|
||||
|
||||
## Types of contributions
|
||||
|
||||
@ -15,13 +15,13 @@ Thanks a lot for your interest in contributing to **divlo.fr**! 🎉
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- **Please first discuss** the change you wish to make via [issue](https://github.com/Divlo/Divlo/issues) before making a change. It might avoid a waste of your time.
|
||||
- **Please first discuss** the change you wish to make via [issue](https://github.com/theoludwig/theoludwig/issues) before making a change. It might avoid a waste of your time.
|
||||
|
||||
- Ensure your code respect linting.
|
||||
|
||||
- Make sure your **code passes the tests**.
|
||||
|
||||
If you're adding new features to **divlo.fr**, please include tests.
|
||||
If you're adding new features to **theoludwig.fr**, please include tests.
|
||||
|
||||
## Commits
|
||||
|
||||
@ -29,27 +29,27 @@ The commit message guidelines adheres to [Conventional Commits](https://www.conv
|
||||
|
||||
## Getting Started
|
||||
|
||||
[](https://gitpod.io/#https://github.com/Divlo/Divlo)
|
||||
[](https://gitpod.io/#https://github.com/theoludwig/theoludwig)
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/) >= 16.0.0
|
||||
- [npm](https://www.npmjs.com/) >= 8.0.0
|
||||
- [Node.js](https://nodejs.org/) >= 20.0.0
|
||||
- [npm](https://www.npmjs.com/) >= 9.0.0
|
||||
|
||||
### Installation
|
||||
|
||||
```sh
|
||||
# Clone the repository
|
||||
git clone https://github.com/Divlo/Divlo.git
|
||||
git clone git@github.com:theoludwig/theoludwig.git
|
||||
|
||||
# Go to the project root
|
||||
cd Divlo
|
||||
cd theoludwig
|
||||
|
||||
# Configure environment variables
|
||||
cp .env.example .env
|
||||
|
||||
# Install
|
||||
npm install
|
||||
npm clean-install
|
||||
```
|
||||
|
||||
### Local Development environment
|
||||
@ -68,4 +68,4 @@ docker compose up --build
|
||||
|
||||
### Services started
|
||||
|
||||
- website: `http://127.0.0.1:3000`
|
||||
- `website`: <http://127.0.0.1:3000>
|
||||
|
@ -1,21 +1,22 @@
|
||||
FROM node:18.16.0 AS builder-dependencies
|
||||
FROM node:20.5.0 AS builder-dependencies
|
||||
WORKDIR /usr/src/application
|
||||
COPY ./package*.json ./
|
||||
RUN npm clean-install
|
||||
|
||||
FROM node:18.16.0 AS builder
|
||||
FROM node:20.5.0 AS builder
|
||||
WORKDIR /usr/src/application
|
||||
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
|
||||
COPY ./ ./
|
||||
RUN npm run build
|
||||
|
||||
FROM gcr.io/distroless/nodejs18-debian11:latest AS runner
|
||||
FROM gcr.io/distroless/nodejs20-debian11:latest AS runner
|
||||
WORKDIR /usr/src/application
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
COPY --from=builder-dependencies /usr/src/application/node_modules ./node_modules
|
||||
COPY --from=builder /usr/src/application/.next/standalone ./
|
||||
COPY --from=builder /usr/src/application/.next/static ./.next/static
|
||||
COPY --from=builder /usr/src/application/public ./public
|
||||
COPY --from=builder /usr/src/application/locales ./locales
|
||||
COPY --from=builder /usr/src/application/i18n/translations ./i18n/translations
|
||||
COPY --from=builder /usr/src/application/next.config.js ./next.config.js
|
||||
CMD ["./server.js"]
|
||||
|
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Divlo
|
||||
Copyright (c) Théo LUDWIG
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
28
README.md
@ -1,18 +1,18 @@
|
||||
<h1 align="center"><a href="https://divlo.fr/">Théo LUDWIG (Divlo)</a></h1>
|
||||
<h1 align="center"><a href="https://theoludwig.fr/">Théo LUDWIG</a></h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Developer Full Stack • Open-Source enthusiast</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Divlo"><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/Divlo"><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/~divlo"><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/Divlo_FR"><img alt="Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=twitter&logoColor=white"/></a>
|
||||
<a href="https://www.youtube.com/channel/UCfEKQzI3c8vmZOrsTOi5spA"><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/divlo"><img alt="Twitch" src="https://img.shields.io/badge/-Twitch-9147FF?style=flat&labelColor=9147FF&logo=twitch&logoColor=white"/></a>
|
||||
<a href="https://www.divlo.fr"><img alt="Website" src="https://img.shields.io/badge/-Website-181818?style=flat&labelColor=181818&logo=Google-Chrome&logoColor=white"/></a>
|
||||
<a href="mailto:contact@divlo.fr"><img alt="Email" src="https://img.shields.io/badge/-contact@divlo.fr-2F7EBE?style=flat&labelColor=2F7EBE&logo=minutemailer&logoColor=white"/></a>
|
||||
<a href="https://github.com/theoludwig"><img alt="GitHub" src="https://img.shields.io/badge/-GitHub-5A5A5A?style=flat&labelColor=5A5A5A&logo=github&logoColor=white"/></a>
|
||||
<a href="https://gitlab.com/theoludwig"><img alt="GitLab" src="https://img.shields.io/badge/-GitLab-303030?style=flat&labelColor=303030&logo=gitlab&logoColor=white"/></a>
|
||||
<a href="https://www.npmjs.com/~theoludwig"><img alt="npm" src="https://img.shields.io/badge/-npm-c4302b?style=flat&labelColor=c4302b&logo=npm&logoColor=white"/></a>
|
||||
<a href="https://twitter.com/theoludwig_"><img alt="Twitter" src="https://img.shields.io/badge/-Twitter-1ca0f1?style=flat&labelColor=1ca0f1&logo=twitter&logoColor=white"/></a>
|
||||
<a href="https://www.youtube.com/@theo_ludwig"><img alt="YouTube" src="https://img.shields.io/badge/-YouTube-c4302b?style=flat&labelColor=c4302b&logo=youtube&logoColor=white"/></a>
|
||||
<a href="https://www.twitch.tv/theoludwig"><img alt="Twitch" src="https://img.shields.io/badge/-Twitch-9147FF?style=flat&labelColor=9147FF&logo=twitch&logoColor=white"/></a>
|
||||
<a href="https://theoludwig.fr/"><img alt="Website" src="https://img.shields.io/badge/-Website-181818?style=flat&labelColor=181818&logo=Google-Chrome&logoColor=white"/></a>
|
||||
<a href="mailto:contact@theoludwig.fr"><img alt="Email" src="https://img.shields.io/badge/-contact@theoludwig.fr-2F7EBE?style=flat&labelColor=2F7EBE&logo=minutemailer&logoColor=white"/></a>
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
@ -21,16 +21,16 @@
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Théo LUDWIG (Divlo)",
|
||||
"name": "Théo LUDWIG",
|
||||
"pronouns": "He/Him",
|
||||
"birthDate": "31/03/2003",
|
||||
"nationality": "Alsace, France",
|
||||
"interests": ["Open-Source enthusiast", "Passionate about High-Tech"],
|
||||
"interests": ["Developer Full Stack", "Open-Source enthusiast"],
|
||||
"skills": {
|
||||
"programmingLanguages": ["JavaScript/TypeScript", "Python", "C/C++", "PHP"],
|
||||
"frontend": ["HTML", "CSS", "Tailwind CSS", "React.js/Next.js"],
|
||||
"backend": ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
|
||||
"tools": ["GNU/Linux", "Ubuntu", "Visual Studio Code", "Git", "Docker"]
|
||||
"tools": ["GNU/Linux", "Arch Linux", "Visual Studio Code", "Git", "Docker"]
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -40,6 +40,6 @@
|
||||
## 📈 Statistics
|
||||
|
||||
<p align=center>
|
||||
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=Divlo&show_icons=true&theme=dark" />
|
||||
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=Divlo&hide=html,css,javascript&langs_count=8&layout=compact&theme=dark" />
|
||||
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api?username=theoludwig&show_icons=true&theme=dark" />
|
||||
<img height=175 align="center" src="https://github-readme-stats.vercel.app/api/top-langs/?username=theoludwig&hide=html,css,javascript&langs_count=8&layout=compact&theme=dark" />
|
||||
</p>
|
||||
|
11
app/blog/[slug]/loading.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Loader } from '@/components/design/Loader'
|
||||
|
||||
const Loading = (): JSX.Element => {
|
||||
return (
|
||||
<main className='flex flex-col flex-1 items-center justify-center'>
|
||||
<Loader />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
44
app/blog/[slug]/page.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
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'
|
||||
|
||||
interface BlogPostPageProps {
|
||||
params: {
|
||||
slug: string
|
||||
}
|
||||
}
|
||||
|
||||
export const generateMetadata = async (
|
||||
props: BlogPostPageProps
|
||||
): Promise<Metadata> => {
|
||||
const blogPost = await getBlogPostBySlug(props.params.slug)
|
||||
if (blogPost == null) {
|
||||
return notFound()
|
||||
}
|
||||
const title = `${blogPost.frontmatter.title} | Théo LUDWIG`
|
||||
const description = blogPost.frontmatter.description
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description
|
||||
},
|
||||
twitter: {
|
||||
title,
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BlogPostPage = async (props: BlogPostPageProps): Promise<JSX.Element> => {
|
||||
const { params } = props
|
||||
|
||||
return <BlogPost slug={params.slug} />
|
||||
}
|
||||
|
||||
export default BlogPostPage
|
11
app/blog/loading.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Loader } from '@/components/design/Loader'
|
||||
|
||||
const Loading = (): JSX.Element => {
|
||||
return (
|
||||
<main className='flex flex-col flex-1 items-center justify-center'>
|
||||
<Loader />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
40
app/blog/page.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
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'>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
|
32
app/error.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
'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-col flex-1 items-center justify-center'>
|
||||
<h1 className='my-6 text-4xl font-semibold'>
|
||||
Error{' '}
|
||||
<span
|
||||
className='text-yellow dark:text-yellow-dark'
|
||||
data-cy='status-code'
|
||||
>
|
||||
500
|
||||
</span>
|
||||
</h1>
|
||||
<p className='text-center text-lg'>Server error</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorHandling
|
@ -8,7 +8,19 @@
|
||||
}
|
||||
|
||||
.prose {
|
||||
@apply !max-w-5xl text-gray dark:text-gray-300;
|
||||
@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,
|
80
app/layout.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
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/icon-96x96.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'
|
||||
},
|
||||
icons: {
|
||||
icon: '/images/icon-96x96.png'
|
||||
},
|
||||
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='bg-white font-headline text-black dark:bg-black dark:text-white flex flex-col min-h-screen'>
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
export default RootLayout
|
11
app/loading.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { Loader } from '@/components/design/Loader'
|
||||
|
||||
const Loading = (): JSX.Element => {
|
||||
return (
|
||||
<main className='flex flex-col flex-1 items-center justify-center'>
|
||||
<Loader />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
32
app/not-found.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { getI18n } from '@/i18n/i18n.server'
|
||||
|
||||
const NotFound = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<main className='flex flex-col flex-1 items-center justify-center'>
|
||||
<h1 className='my-6 text-4xl font-semibold'>
|
||||
{i18n.translate('errors.error')}{' '}
|
||||
<span
|
||||
className='text-yellow dark:text-yellow-dark'
|
||||
data-cy='status-code'
|
||||
>
|
||||
404
|
||||
</span>
|
||||
</h1>
|
||||
<p className='text-center text-lg'>
|
||||
{i18n.translate('errors.not-found')}{' '}
|
||||
<Link
|
||||
href='/'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
>
|
||||
{i18n.translate('errors.return-to-home-page')}
|
||||
</Link>
|
||||
</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotFound
|
59
app/page.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
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
|
35
blog/BlogPost.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import date from 'date-and-time'
|
||||
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
import { getBlogPostBySlug } from '@/blog/blog'
|
||||
import { BlogPostContent } from '@/blog/BlogPostContent'
|
||||
|
||||
export interface BlogPostProps {
|
||||
slug: string
|
||||
}
|
||||
|
||||
export const BlogPost = async (props: BlogPostProps): Promise<JSX.Element> => {
|
||||
const { slug } = props
|
||||
|
||||
const blogPost = await getBlogPostBySlug(slug)
|
||||
if (blogPost == null) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<main className='break-wrap-words flex flex-1 flex-col flex-wrap items-center justify-center'>
|
||||
<div className='my-10 flex flex-col items-center text-center'>
|
||||
<h1 className='text-3xl font-semibold'>{blogPost.frontmatter.title}</h1>
|
||||
<p className='mt-2' data-cy='blog-post-date'>
|
||||
{date.format(
|
||||
new Date(blogPost.frontmatter.publishedOn),
|
||||
'DD/MM/YYYY'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<BlogPostContent content={blogPost.content} />
|
||||
</main>
|
||||
)
|
||||
}
|
33
blog/BlogPostComments.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
|
||||
import Giscus from '@giscus/react'
|
||||
|
||||
import { useTheme } from '@/theme/theme.client'
|
||||
import type { CookiesStore } from '@/utils/constants'
|
||||
|
||||
interface BlogPostCommentsProps {
|
||||
cookiesStore: CookiesStore
|
||||
}
|
||||
|
||||
export const BlogPostComments = (props: BlogPostCommentsProps): JSX.Element => {
|
||||
const { cookiesStore } = props
|
||||
|
||||
const theme = useTheme(cookiesStore)
|
||||
|
||||
return (
|
||||
<Giscus
|
||||
id='comments'
|
||||
repo='theoludwig/theoludwig'
|
||||
repoId='MDEwOlJlcG9zaXRvcnkzNTg5NDg1NDQ='
|
||||
category='General'
|
||||
categoryId='DIC_kwDOFWUewM4CQ_WK'
|
||||
mapping='pathname'
|
||||
reactionsEnabled='1'
|
||||
emitMetadata='0'
|
||||
inputPosition='top'
|
||||
theme={theme}
|
||||
lang='en'
|
||||
loading='lazy'
|
||||
/>
|
||||
)
|
||||
}
|
111
blog/BlogPostContent.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { cookies } from 'next/headers'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faLink } from '@fortawesome/free-solid-svg-icons'
|
||||
import { MDXRemote } from 'next-mdx-remote/rsc'
|
||||
import { nodeTypes } from '@mdx-js/mdx'
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import remarkMath from 'remark-math'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import { getHighlighter } from 'shiki'
|
||||
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
import { getTheme } from '@/theme/theme.server'
|
||||
import { remarkSyntaxHighlightingPlugin } from '@/blog/remarkSyntaxHighlightingPlugin'
|
||||
import { BlogPostComments } from '@/blog/BlogPostComments'
|
||||
|
||||
const Heading = (
|
||||
props: React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLHeadingElement>,
|
||||
HTMLHeadingElement
|
||||
>
|
||||
): JSX.Element => {
|
||||
const { children, id = '' } = props
|
||||
return (
|
||||
<h2 {...props} className='group'>
|
||||
<Link
|
||||
href={`#${id}`}
|
||||
className='invisible !text-black group-hover:visible dark:!text-white'
|
||||
>
|
||||
<FontAwesomeIcon className='mr-2 inline h-4 w-4' icon={faLink} />
|
||||
</Link>
|
||||
{children}
|
||||
</h2>
|
||||
)
|
||||
}
|
||||
|
||||
export interface BlogPostContentProps {
|
||||
content: string
|
||||
}
|
||||
|
||||
export const BlogPostContent = async (
|
||||
props: BlogPostContentProps
|
||||
): Promise<JSX.Element> => {
|
||||
const { content } = props
|
||||
|
||||
const cookiesStore = cookies()
|
||||
const theme = getTheme()
|
||||
|
||||
const highlighter = await getHighlighter({
|
||||
theme: `${theme}-plus`
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='prose mb-10'>
|
||||
<div className='px-8'>
|
||||
<MDXRemote
|
||||
source={content}
|
||||
options={{
|
||||
mdxOptions: {
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
[remarkSyntaxHighlightingPlugin, { highlighter }],
|
||||
remarkMath
|
||||
],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
[rehypeRaw, { passThrough: nodeTypes }],
|
||||
rehypeKatex
|
||||
]
|
||||
}
|
||||
}}
|
||||
components={{
|
||||
h1: Heading,
|
||||
h2: Heading,
|
||||
h3: Heading,
|
||||
h4: Heading,
|
||||
h5: Heading,
|
||||
h6: Heading,
|
||||
img: (properties) => {
|
||||
const { src = '', alt = 'Blog Image' } = properties
|
||||
const source = src.replace('../../public/', '/')
|
||||
return (
|
||||
<span className='flex flex-col items-center justify-center'>
|
||||
<Image
|
||||
src={source}
|
||||
alt={alt}
|
||||
width={1000}
|
||||
height={1000}
|
||||
className='h-auto w-auto'
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
a: (props) => {
|
||||
const { href = '' } = props
|
||||
if (href.startsWith('#')) {
|
||||
return <a {...props} />
|
||||
}
|
||||
return <a target='_blank' rel='noopener noreferrer' {...props} />
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<BlogPostComments cookiesStore={cookiesStore.toString()} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
42
blog/BlogPosts.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import Link from 'next/link'
|
||||
import date from 'date-and-time'
|
||||
|
||||
import { ShadowContainer } from '@/components/design/ShadowContainer'
|
||||
import { getBlogPosts } from '@/blog/blog'
|
||||
|
||||
export const BlogPosts = async (): Promise<JSX.Element> => {
|
||||
const posts = await getBlogPosts()
|
||||
|
||||
return (
|
||||
<div className='flex w-full items-center justify-center p-8'>
|
||||
<div className='w-[1600px]' data-cy='blog-posts'>
|
||||
{posts.map((post, index) => {
|
||||
const postPublishedOn = date.format(
|
||||
new Date(post.frontmatter.publishedOn),
|
||||
'DD/MM/YYYY'
|
||||
)
|
||||
return (
|
||||
<Link
|
||||
href={`/blog/${post.slug}`}
|
||||
key={index}
|
||||
locale='en'
|
||||
data-cy={post.slug}
|
||||
>
|
||||
<ShadowContainer className='cursor-pointer p-6 transition duration-200 ease-in-out hover:-translate-y-2'>
|
||||
<h2 data-cy='blog-post-title' className='text-xl font-semibold'>
|
||||
{post.frontmatter.title}
|
||||
</h2>
|
||||
<p data-cy='blog-post-date' className='mt-2'>
|
||||
{postPublishedOn}
|
||||
</p>
|
||||
<p data-cy='blog-post-description' className='mt-3'>
|
||||
{post.frontmatter.description}
|
||||
</p>
|
||||
</ShadowContainer>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
65
blog/blog.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { cache } from 'react'
|
||||
import matter from 'gray-matter'
|
||||
|
||||
export const BLOG_POSTS_PATH = path.join(process.cwd(), 'blog', 'posts')
|
||||
|
||||
export interface FrontMatter {
|
||||
title: string
|
||||
description: string
|
||||
isPublished: boolean
|
||||
publishedOn: string
|
||||
}
|
||||
|
||||
export interface BlogPost {
|
||||
frontmatter: FrontMatter
|
||||
slug: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export const getBlogPosts = cache(async (): Promise<BlogPost[]> => {
|
||||
const blogPosts = await fs.promises.readdir(BLOG_POSTS_PATH)
|
||||
const blogPostsWithTime = await Promise.all(
|
||||
blogPosts.map(async (blogPostFilename) => {
|
||||
const [slug, extension] = blogPostFilename.split('.')
|
||||
if (slug == null || extension == null) {
|
||||
throw new Error('Invalid blog post filename.')
|
||||
}
|
||||
const blogPostPath = path.join(BLOG_POSTS_PATH, `${slug}.${extension}`)
|
||||
const blogPostContent = await fs.promises.readFile(blogPostPath, {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
const { data, content } = matter(blogPostContent) as unknown as {
|
||||
data: FrontMatter
|
||||
content: string
|
||||
}
|
||||
const date = new Date(data.publishedOn)
|
||||
return {
|
||||
slug,
|
||||
content,
|
||||
frontmatter: data,
|
||||
time: date.getTime()
|
||||
}
|
||||
})
|
||||
)
|
||||
const blogPostsSortedByPublicationDate = blogPostsWithTime
|
||||
.filter((post) => {
|
||||
return post.frontmatter.isPublished
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return b.time - a.time
|
||||
})
|
||||
return blogPostsSortedByPublicationDate
|
||||
})
|
||||
|
||||
export const getBlogPostBySlug = cache(
|
||||
async (slug: string): Promise<BlogPost | undefined> => {
|
||||
const blogPosts = await getBlogPosts()
|
||||
const blogPost = blogPosts.find((blogPost) => {
|
||||
return blogPost.slug === slug && blogPost.frontmatter.isPublished
|
||||
})
|
||||
return blogPost
|
||||
}
|
||||
)
|
@ -21,7 +21,7 @@ Git was originally authored by [Linus Torvalds](https://en.wikipedia.org/wiki/Li
|
||||
|
||||
Git allows:
|
||||
|
||||
- to be able to work with several people on the same codebase.
|
||||
- to work with several people on the same codebase.
|
||||
- track changes to know who did what and when.
|
||||
- revert changes.
|
||||
|
||||
@ -122,6 +122,18 @@ git checkout <branch>
|
||||
# Merge a branch into the current branch
|
||||
git merge <branch>
|
||||
|
||||
# Note: Merge creates a "Merge commit" when the base branch and the branch to merge have diverged (they have different commits).
|
||||
|
||||
# To avoid creating a "Merge commit", we can use rebase instead of merge.
|
||||
git rebase --interactive <branch-to-rebase-on>
|
||||
|
||||
# Combine multiple commits of a branch into one for a merge
|
||||
git merge --squash <branch>
|
||||
|
||||
# Change several past commits (interactive rebase)
|
||||
# HEAD points to the current consulted commit.
|
||||
git rebase --interactive HEAD~<number-of-commits>
|
||||
|
||||
# Delete a branch
|
||||
git branch --delete <branch>
|
||||
git push <remote> --delete <branch>
|
||||
@ -132,15 +144,19 @@ git fetch --prune
|
||||
# Revert a commit
|
||||
git revert <commit>
|
||||
|
||||
# Change several past commits (interactive rebase)
|
||||
# HEAD points to the current consulted commit.
|
||||
git rebase --interactive HEAD~<number-of-commits>
|
||||
|
||||
# Reset the current branch, delete all commits since <branch> (without removing the changes)
|
||||
git reset --soft <branch>
|
||||
|
||||
# Apply the changes introduced by some existing commits
|
||||
# (by first being on the branch where you want to apply the commit)
|
||||
git cherry-pick <commit>
|
||||
|
||||
# To list all commits that differ between two branches
|
||||
git log <branch1>..<branch2> # commits in branch2 that are not in branch1 (branch2 ahead of branch1, branch2 behind branch1)
|
||||
git log <branch2>..<branch1> # commits in branch1 that are not in branch2 (branch1 ahead of branch2, branch1 behind branch2)
|
||||
|
||||
# Summary of commit authors across all branches, excluding merge commits.
|
||||
git shortlog --summary --numbered --all --no-merges
|
||||
```
|
||||
|
||||
## `.gitignore` file
|
||||
@ -190,7 +206,7 @@ As we have seen in the [Get started with `git` and `.gitconfig` config file](#ge
|
||||
|
||||
That means that **anyone can create a commit with any name and email address and claim to be whoever they want** when they create a commit.
|
||||
|
||||
To avoid this, you can sign your commits with a <abbr title="GNU Privacy Guard">[GPG](https://gnupg.org/)</abbr> key.
|
||||
To avoid this, you can sign your commits with a [GNU Privacy Guard](https://gnupg.org/) (<abbr>gpg</abbr>) key.
|
||||
|
||||
You can find more information about signing commits in the [official documentation](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work).
|
||||
|
@ -13,11 +13,11 @@ This blog is here to document my journey of learning computer science, explainin
|
||||
|
||||
The idea is that I will share my knowledge with you (readers), and hopefully help you to learn too.
|
||||
|
||||
Keep in mind that I will not translate the posts in French, all the posts will be written in English, as I'm not a native English speaker, I will probably make mistakes, feel free to open pull requests on [GitHub](https://github.com/Divlo/Divlo) to correct them. 😊
|
||||
Keep in mind that I will not translate the posts in French, all the posts will be written in English, as I'm not a native English speaker, I will probably make mistakes, feel free to open pull requests on [GitHub](https://github.com/theoludwig/theoludwig) to correct them. 😊
|
||||
|
||||
I plan to publish new posts when I have something new to share. There's no schedule, so stay tuned!
|
||||
|
||||
To stay informed of new blog post and to ask questions, feel free to follow me on Twitter: [@Divlo_FR](https://twitter.com/Divlo_FR).
|
||||
To stay informed of new blog post and to ask questions, feel free to follow me on Twitter: [@theoludwig\_](https://twitter.com/theoludwig_).
|
||||
|
||||
## Project based learning
|
||||
|
||||
@ -33,7 +33,7 @@ I learn something new, because it solved a "real life" problem I had encountered
|
||||
|
||||
In this section, I will explain what technologies I used to make this blog, and what are the technical choices I had to do.
|
||||
|
||||
The code of this website is open source on [GitHub](https://github.com/Divlo/Divlo), so you can see the code and contribute to it.
|
||||
The code of this website is open source on [GitHub](https://github.com/theoludwig/theoludwig), so you can see the code and contribute to it.
|
||||
|
||||
### Technologies
|
||||
|
@ -41,13 +41,13 @@ Find the right balance, between abstraction and simple implementation, start sim
|
||||
|
||||
When you start a new project, you should focus on the core of the project, not on the details, to release as soon as possible, a working usable version of your project also called a [**Minimum Viable Product** (MVP)](https://en.wikipedia.org/wiki/Minimum_viable_product), it is better than a half-functioning, over-engineered project.
|
||||
|
||||
I made this mistake while developing [Thream](https://thream.divlo.fr), your **open source** platform to stay close with your friends and communities, **talk**, chat, **collaborate**, share and **have fun**.
|
||||
I made this mistake while developing [Thream](https://thream.theoludwig.fr), your **open source** platform to stay close with your friends and communities, **talk**, chat, **collaborate**, share and **have fun**.
|
||||
|
||||
Basically, I thought it was cool, to do a "big" v1.0.0 release with a lot of features, but in fact, it was not, because I could not even show what I was developing (to the end-users, not technical people) as I was making multiple features at the same time and also mainly focused on the **REST API** side and not at all the **website (frontend)**.
|
||||
|
||||
What I recommend you to do is to start with a **v1.0.0** release as soon as possible with the minimum required features needed for your project idea, and then gradually add new features and release new versions.
|
||||
|
||||
In my example for [Thream](https://thream.divlo.fr), I could release a v1.0.0 without these features:
|
||||
In my example for [Thream](https://thream.theoludwig.fr), I could release a v1.0.0 without these features:
|
||||
|
||||
- English/French translation (could be only English)
|
||||
- Light/Dark theme (could be only Dark)
|
||||
@ -55,7 +55,7 @@ In my example for [Thream](https://thream.divlo.fr), I could release a v1.0.0 wi
|
||||
- User public profile
|
||||
- Channels (maybe could be only one channel per guild to start with)
|
||||
|
||||
And probably more, what was really required with [Thream](https://thream.divlo.fr), is that users could authenticate, create a community of friends, and then they could communicate with each other with messages in real-time, really that was enough.
|
||||
And probably more, what was really required with [Thream](https://thream.theoludwig.fr), is that users could authenticate, create a community of friends, and then they could communicate with each other with messages in real-time, really that was enough.
|
||||
|
||||
And then with this basis, I could release, v1.1.0, v1.2.0 etc. with more features, and release new versions more often to show the progress of the project, it is also more motivating to have users testing our project and to **get feedback sooner**.
|
||||
|
@ -216,7 +216,7 @@ $$
|
||||
|
||||
#### Complexity Classes (from fastest to slowest)
|
||||
|
||||

|
||||

|
||||
|
||||
Here is a list of classes of functions that are commonly encountered when analyzing the running time of an algorithm.
|
||||
|
||||
@ -333,11 +333,11 @@ def maximum_subarray_sum_linear(array: list[int]) -> int:
|
||||
|
||||
Problems solving is a very complicated and large topic, and also a very important skill to have as a software developer.
|
||||
|
||||
To improve our problems solving skills, we can regularly practice with [programming challenges](https://github.com/Divlo/programming-challenges).
|
||||
To improve our problems solving skills, we can regularly practice with [programming challenges](https://github.com/theoludwig/programming-challenges).
|
||||
|
||||
## Sources
|
||||
|
||||
- [Wikipedia - Competitive programming](https://en.wikipedia.org/wiki/Competitive_programming)
|
||||
- [Frontend Masters - The Last Algorithms Course You'll Need](https://frontendmasters.com/courses/algorithms/)
|
||||
- [Big-O Cheat Sheet](https://www.bigocheatsheet.com/)
|
||||
- [programming challenges](https://github.com/Divlo/programming-challenges)
|
||||
- [programming challenges](https://github.com/theoludwig/programming-challenges)
|
@ -7,13 +7,13 @@ publishedOn: '2022-04-11T10:24:55.206Z'
|
||||
|
||||
Hello! 👋
|
||||
|
||||
After months of hard work, [Thream v1.0.0](https://www.thream.divlo.fr/) has been released! 🎉
|
||||
After months of hard work, [Thream v1.0.0](https://thream.theoludwig.fr/) has been released! 🎉
|
||||
|
||||
[**Thream**](https://www.thream.divlo.fr/) is your open-source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.
|
||||
[**Thream**](https://thream.theoludwig.fr/) is your open-source platform to stay close with your friends and communities, talk, chat, collaborate, share and have fun.
|
||||
|
||||
## Presentation
|
||||
|
||||
[**Thream**](https://www.thream.divlo.fr/) is a social network to stay close with your friends and communities to talk, chat, collaborate and share.
|
||||
[**Thream**](https://thream.theoludwig.fr/) is a social network to stay close with your friends and communities to talk, chat, collaborate and share.
|
||||
|
||||
The project is largely inspired by [Discord](https://discord.com), a proprietary instant messaging service, but differentiates itself by its **non-profit open source philosophy** and will integrate special features.
|
||||
|
||||
@ -21,19 +21,19 @@ The source code is available on [GitHub](https://github.com/Thream).
|
||||
|
||||
The idea is that a user can create an account to authenticate with an email address, and a password, or directly use an account from another platform (currently supported: Google, GitHub, Discord). Once the user is authenticated, he/she can create and join "guilds", in other words communities, in order to discuss with other people in several channels to group discussions talking about the same subject.
|
||||
|
||||

|
||||

|
||||
|
||||
[**Thream**](https://www.thream.divlo.fr/) is a website that works on any recent browser, accessible on [thream.divlo.fr](https://www.thream.divlo.fr/).
|
||||
[**Thream**](https://thream.theoludwig.fr/) is a website that works on any recent browser, accessible on [thream.theoludwig.fr](https://thream.theoludwig.fr/).
|
||||
|
||||
## History
|
||||
|
||||
The idea for the project has existed since May 13, 2020, symbolized by a [publication on Twitter](https://twitter.com/Divlo_FR/status/1260638175246135296) by the creator: Divlo.
|
||||
The idea for the project has existed since May 13, 2020, symbolized by a [publication on Twitter](https://twitter.com/theoludwig_/status/1260638175246135296) by the creator: Théo LUDWIG.
|
||||
|
||||
The main goal is to put into **practice knowledge in web development** and computer science in general on a concrete project that can **easily evolve over time** where you can add many features.
|
||||
|
||||
The development of the project begins under the name of **SocialProject**, on August 20, 2020, with colors close to the image of Divlo.
|
||||
The development of the project begins under the name of **SocialProject**, on August 20, 2020.
|
||||
|
||||

|
||||

|
||||
|
||||
When I started the project, I had little knowledge of database design, real-time management or the architecture of such a large <abbr title="Information Technology">IT</abbr> project, so this will be accompanied by many technical problems, to which we will need to find appropriate solutions.
|
||||
|
||||
@ -53,7 +53,7 @@ Since the project is mainly developed during free time (mainly on weekends), the
|
||||
|
||||
- The **client** part, called **frontend**, what **the user sees on the screen**, such as forms, buttons and all the **graphic elements** with which the user can interact from a browser.
|
||||
|
||||

|
||||

|
||||
|
||||
This design allows the separation between the client and the server, as long as they both structure their communication according to the <abbr title="Representational state transfer">REST</abbr> architectural guidelines, using the <abbr title="Hypertext Transfer Protocol">HTTP</abbr> protocol, they will be able to communicate with each other, which makes it possible to work independently on the backend and on the frontend using different technologies and skills, really useful in teamwork.
|
||||
|
||||
@ -116,4 +116,4 @@ The other interest of the project is that it is completely **open-source**, and
|
||||
|
||||
Feel free to give feebacks and suggestions to improve the project, and to report any bug you find.
|
||||
|
||||
**Thream** is available: [**thream.divlo.fr**](https://www.thream.divlo.fr/).
|
||||
**Thream** is available: [**thream.theoludwig.fr**](https://thream.theoludwig.fr/).
|
@ -1,5 +1,5 @@
|
||||
import type { Plugin, Transformer } from 'unified'
|
||||
import type { Literal } from 'unist'
|
||||
import type { Literal, Node } from 'unist'
|
||||
import { visit } from 'unist-util-visit'
|
||||
import type { Highlighter } from 'shiki'
|
||||
|
||||
@ -7,15 +7,17 @@ export interface RemarkSyntaxHighlightingPluginOptions {
|
||||
highlighter: Highlighter
|
||||
}
|
||||
|
||||
export type RemarkSyntaxHighlightingNode = Literal<string> & {
|
||||
export interface RemarkSyntaxHighlightingNode extends Node {
|
||||
lang: string
|
||||
meta: string
|
||||
children: undefined
|
||||
value: string
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
|
||||
export const remarkSyntaxHighlightingPlugin: Plugin<
|
||||
[RemarkSyntaxHighlightingPluginOptions],
|
||||
Literal<string, RemarkSyntaxHighlightingNode>
|
||||
Literal
|
||||
> = (options) => {
|
||||
const transformer: Transformer<RemarkSyntaxHighlightingNode> = (tree) => {
|
||||
visit<RemarkSyntaxHighlightingNode, string>(tree, 'code', (node) => {
|
@ -1,45 +0,0 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import Link from 'next/link'
|
||||
|
||||
import type { FooterProps } from './Footer'
|
||||
import { Footer } from './Footer'
|
||||
import { Header } from './Header'
|
||||
|
||||
export interface ErrorPageProps extends FooterProps {
|
||||
statusCode: number
|
||||
message: string
|
||||
}
|
||||
|
||||
export const ErrorPage: React.FC<ErrorPageProps> = (props) => {
|
||||
const { message, statusCode, version } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex h-screen flex-col pt-0'>
|
||||
<Header showLanguage />
|
||||
<main className='flex min-w-full flex-1 flex-col items-center justify-center'>
|
||||
<h1 className='my-6 text-4xl font-semibold'>
|
||||
{t('errors:error')}{' '}
|
||||
<span
|
||||
className='text-yellow dark:text-yellow-dark'
|
||||
data-cy='status-code'
|
||||
>
|
||||
{statusCode}
|
||||
</span>
|
||||
</h1>
|
||||
<p className='text-center text-lg'>
|
||||
{message}{' '}
|
||||
<Link
|
||||
href='/'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
>
|
||||
{t('errors:return-to-home-page')}
|
||||
</Link>
|
||||
</p>
|
||||
</main>
|
||||
<Footer version={version} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
import { useMemo } from 'react'
|
||||
import Link from 'next/link'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
|
||||
export interface FooterProps {
|
||||
version: string
|
||||
}
|
||||
|
||||
export const Footer: React.FC<FooterProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { version } = props
|
||||
|
||||
const versionLink = useMemo(() => {
|
||||
return `https://github.com/Divlo/Divlo/releases/tag/v${version}`
|
||||
}, [version])
|
||||
|
||||
return (
|
||||
<footer className='flex flex-col items-center justify-center border-t-2 border-gray-600 bg-white py-6 text-lg dark:border-gray-400 dark:bg-black'>
|
||||
<p>
|
||||
<Link
|
||||
href='/'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
>
|
||||
Théo LUDWIG (Divlo)
|
||||
</Link>{' '}
|
||||
| {t('common:all-rights-reserved')}
|
||||
</p>
|
||||
<p className='mt-1'>
|
||||
Version{' '}
|
||||
<a
|
||||
data-cy='version-link'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
href={versionLink}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
{version}
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
)
|
||||
}
|
19
components/Footer/FooterText.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { getI18n } from '@/i18n/i18n.server'
|
||||
|
||||
export const FooterText = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<p>
|
||||
<Link
|
||||
href='/'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
>
|
||||
Théo LUDWIG
|
||||
</Link>{' '}
|
||||
| {i18n.translate('common.all-rights-reserved')}
|
||||
</p>
|
||||
)
|
||||
}
|
28
components/Footer/FooterVersion.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { useMemo } from 'react'
|
||||
|
||||
interface FooterVersionProps {
|
||||
version: string
|
||||
}
|
||||
|
||||
export const FooterVersion = (props: FooterVersionProps): JSX.Element => {
|
||||
const { version } = props
|
||||
|
||||
const versionLink = useMemo(() => {
|
||||
return `https://github.com/theoludwig/theoludwig/releases/tag/v${version}`
|
||||
}, [version])
|
||||
|
||||
return (
|
||||
<p className='mt-1'>
|
||||
Version{' '}
|
||||
<a
|
||||
data-cy='version-link'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
href={versionLink}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
{version}
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
}
|
14
components/Footer/index.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { FooterText } from './FooterText'
|
||||
import { FooterVersion } from './FooterVersion'
|
||||
|
||||
export const Footer = async (): Promise<JSX.Element> => {
|
||||
const { readPackage } = await import('read-pkg')
|
||||
const { version } = await readPackage()
|
||||
|
||||
return (
|
||||
<footer className='flex flex-col items-center justify-center border-t-2 border-gray-600 bg-white py-6 text-lg dark:border-gray-400 dark:bg-black'>
|
||||
<FooterText />
|
||||
<FooterVersion version={version} />
|
||||
</footer>
|
||||
)
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import NextHead from 'next/head'
|
||||
|
||||
interface HeadProps {
|
||||
title?: string
|
||||
image?: string
|
||||
description?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
export const Head: React.FC<HeadProps> = (props) => {
|
||||
const {
|
||||
title = 'Théo LUDWIG (Divlo)',
|
||||
image = 'https://divlo.fr/images/icon-96x96.png',
|
||||
description = 'Théo LUDWIG (Divlo) - Developer Full Stack • Passionate about High-Tech',
|
||||
url = 'https://divlo.fr/'
|
||||
} = props
|
||||
|
||||
return (
|
||||
<NextHead>
|
||||
<title>{title}</title>
|
||||
<link rel='icon' type='image/png' href={image} />
|
||||
|
||||
{/* Meta Tag */}
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
|
||||
<meta name='description' content={description} />
|
||||
<meta name='Language' content='fr-FR, en-US' />
|
||||
<meta name='theme-color' content='#ffd800' />
|
||||
|
||||
{/* Open Graph Metadata */}
|
||||
<meta property='og:title' content={title} />
|
||||
<meta property='og:type' content='website' />
|
||||
<meta property='og:url' content={url} />
|
||||
<meta property='og:image' content={image} />
|
||||
<meta property='og:description' content={description} />
|
||||
<meta property='og:locale' content='fr-FR, en-US' />
|
||||
<meta property='og:site_name' content={title} />
|
||||
|
||||
{/* Twitter card Metadata */}
|
||||
<meta name='twitter:card' content='summary' />
|
||||
<meta name='twitter:description' content={description} />
|
||||
<meta name='twitter:title' content={title} />
|
||||
<meta name='twitter:image' content={image} />
|
||||
|
||||
{/* Google Verification */}
|
||||
<meta
|
||||
name='google-site-verification'
|
||||
content='j9CQEbSuYydXytr6gdkTfam_xX_pU97NSpVH3Bq-6f4'
|
||||
/>
|
||||
</NextHead>
|
||||
)
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
export interface LanguageFlagProps {
|
||||
language: string
|
||||
}
|
||||
|
||||
export const LanguageFlag: React.FC<LanguageFlagProps> = (props) => {
|
||||
const { language } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
quality={100}
|
||||
width={35}
|
||||
height={35}
|
||||
src={`/images/languages/${language}.svg`}
|
||||
alt={language}
|
||||
/>
|
||||
<p data-cy='language-flag-text' className='mx-2 text-base'>
|
||||
{language.toUpperCase()}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export const Arrow: React.FC = () => {
|
||||
export const Arrow = (): JSX.Element => {
|
||||
return (
|
||||
<svg
|
||||
width='12'
|
30
components/Header/Locales/LocaleFlag.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
import type { CookiesStore } from '@/utils/constants'
|
||||
import { useI18n } from '@/i18n/i18n.client'
|
||||
|
||||
export interface LocaleFlagProps {
|
||||
locale: string
|
||||
cookiesStore: CookiesStore
|
||||
}
|
||||
|
||||
export const LocaleFlag = (props: LocaleFlagProps): JSX.Element => {
|
||||
const { locale, cookiesStore } = props
|
||||
|
||||
const i18n = useI18n(cookiesStore)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
quality={100}
|
||||
width={35}
|
||||
height={35}
|
||||
src={`/images/locales/${locale}.svg`}
|
||||
alt={locale}
|
||||
/>
|
||||
<p data-cy='locale-flag-text' className='mx-2 text-base'>
|
||||
{i18n.translate(`common.${locale}`)}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,15 +1,24 @@
|
||||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useCallback, useEffect, useState, useRef } from 'react'
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import setLanguage from 'next-translate/setLanguage'
|
||||
import classNames from 'clsx'
|
||||
|
||||
import i18n from 'i18n.json'
|
||||
import type { Locale as LocaleType, CookiesStore } from '@/utils/constants'
|
||||
import { LOCALES } from '@/utils/constants'
|
||||
|
||||
import { Arrow } from './Arrow'
|
||||
import { LanguageFlag } from './LanguageFlag'
|
||||
import { LocaleFlag } from './LocaleFlag'
|
||||
|
||||
export interface LocalesProps {
|
||||
currentLocale: string
|
||||
cookiesStore: CookiesStore
|
||||
}
|
||||
|
||||
export const Locales = (props: LocalesProps): JSX.Element => {
|
||||
const { currentLocale, cookiesStore } = props
|
||||
const pathname = usePathname()
|
||||
|
||||
export const Language: React.FC = () => {
|
||||
const { lang: currentLanguage } = useTranslation()
|
||||
const [hiddenMenu, setHiddenMenu] = useState(true)
|
||||
const languageClickRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
@ -36,42 +45,52 @@ export const Language: React.FC = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLanguage = async (language: string): Promise<void> => {
|
||||
await setLanguage(language)
|
||||
const handleLocale = async (locale: LocaleType): Promise<void> => {
|
||||
const { setLocale } = await import('@/i18n/i18n.server')
|
||||
setLocale(locale)
|
||||
}
|
||||
|
||||
if (pathname.startsWith('/blog')) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex cursor-pointer flex-col items-center justify-center'>
|
||||
<div
|
||||
ref={languageClickRef}
|
||||
data-cy='language-click'
|
||||
data-cy='locale-click'
|
||||
className='mr-5 flex items-center'
|
||||
onClick={handleHiddenMenu}
|
||||
>
|
||||
<LanguageFlag language={currentLanguage} />
|
||||
<LocaleFlag
|
||||
locale={currentLocale}
|
||||
cookiesStore={cookiesStore?.toString()}
|
||||
/>
|
||||
<Arrow />
|
||||
</div>
|
||||
|
||||
<ul
|
||||
data-cy='languages-list'
|
||||
data-cy='locales-list'
|
||||
className={classNames(
|
||||
'absolute top-14 z-10 mr-4 mt-3 flex w-24 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
|
||||
'absolute top-14 z-10 mr-4 mt-3 flex w-32 list-none flex-col items-center justify-center rounded-lg bg-white p-0 shadow-lightFlag dark:bg-black dark:shadow-darkFlag',
|
||||
{ hidden: hiddenMenu }
|
||||
)}
|
||||
>
|
||||
{i18n.locales.map((language, index) => {
|
||||
if (language === currentLanguage) {
|
||||
return null
|
||||
}
|
||||
{LOCALES.filter((locale) => {
|
||||
return locale !== currentLocale
|
||||
}).map((locale) => {
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className='flex h-12 w-full items-center justify-center pl-2 hover:bg-[#4f545c] hover:bg-opacity-20'
|
||||
key={locale}
|
||||
className='flex h-12 w-full items-center justify-center hover:bg-[#4f545c] hover:bg-opacity-20'
|
||||
onClick={async () => {
|
||||
return await handleLanguage(language)
|
||||
return await handleLocale(locale)
|
||||
}}
|
||||
>
|
||||
<LanguageFlag language={language} />
|
||||
<LocaleFlag
|
||||
locale={locale}
|
||||
cookiesStore={cookiesStore?.toString()}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
@ -1,21 +1,22 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
'use client'
|
||||
|
||||
import classNames from 'clsx'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
export const SwitchTheme: React.FC = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme, setTheme } = useTheme()
|
||||
import { useTheme } from '@/theme/theme.client'
|
||||
import type { CookiesStore } from '@/utils/constants'
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
export interface SwitchThemeProps {
|
||||
cookiesStore: CookiesStore
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
export const SwitchTheme = (props: SwitchThemeProps): JSX.Element => {
|
||||
const { cookiesStore } = props
|
||||
const theme = useTheme(cookiesStore)
|
||||
|
||||
const handleClick = (): void => {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark')
|
||||
const handleClick = async (): Promise<void> => {
|
||||
const { setTheme } = await import('@/theme/theme.server')
|
||||
const newTheme = theme === 'dark' ? 'light' : 'dark'
|
||||
setTheme(newTheme)
|
||||
}
|
||||
|
||||
return (
|
||||
@ -69,7 +70,7 @@ export const SwitchTheme: React.FC = () => {
|
||||
data-cy='switch-theme-input'
|
||||
type='checkbox'
|
||||
aria-label='Dark mode toggle'
|
||||
className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0'
|
||||
className='absolute m-[-1px] h-[1px] w-[1px] overflow-hidden border-0 p-0 hidden'
|
||||
defaultChecked
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { cookies } from 'next/headers'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
|
||||
import { Language } from './Language'
|
||||
import { getI18n } from '@/i18n/i18n.server'
|
||||
|
||||
import { Locales } from './Locales'
|
||||
import { SwitchTheme } from './SwitchTheme'
|
||||
|
||||
export interface HeaderProps {
|
||||
showLanguage?: boolean
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = (props) => {
|
||||
const { showLanguage = false } = props
|
||||
export const Header = (): JSX.Element => {
|
||||
const cookiesStore = cookies()
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<header className='sticky top-0 z-50 flex w-full justify-between border-b-2 border-gray-600 bg-white px-6 py-2 dark:border-gray-400 dark:bg-black'>
|
||||
@ -19,12 +19,12 @@ export const Header: React.FC<HeaderProps> = (props) => {
|
||||
quality={100}
|
||||
width={60}
|
||||
height={60}
|
||||
src='/images/divlo_icon_small.png'
|
||||
alt='Divlo'
|
||||
src='/images/icon_small.png'
|
||||
alt='Théo LUDWIG'
|
||||
priority
|
||||
/>
|
||||
<strong className='ml-1 hidden font-headline font-semibold text-yellow dark:text-yellow-dark xs:block'>
|
||||
Théo LUDWIG (Divlo)
|
||||
Théo LUDWIG
|
||||
</strong>
|
||||
</div>
|
||||
</Link>
|
||||
@ -38,8 +38,11 @@ export const Header: React.FC<HeaderProps> = (props) => {
|
||||
Blog
|
||||
</Link>
|
||||
</div>
|
||||
{showLanguage ? <Language /> : null}
|
||||
<SwitchTheme />
|
||||
<Locales
|
||||
currentLocale={i18n.locale}
|
||||
cookiesStore={cookiesStore.toString()}
|
||||
/>
|
||||
<SwitchTheme cookiesStore={cookiesStore.toString()} />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
@ -5,7 +5,9 @@ export interface InterestParagraphProps {
|
||||
description: string
|
||||
}
|
||||
|
||||
export const InterestParagraph: React.FC<InterestParagraphProps> = (props) => {
|
||||
export const InterestParagraph = (
|
||||
props: InterestParagraphProps
|
||||
): JSX.Element => {
|
||||
const { title, description } = props
|
||||
|
||||
return (
|
||||
|
@ -6,7 +6,7 @@ interface InterestItemProps {
|
||||
fontAwesomeIcon: IconDefinition
|
||||
}
|
||||
|
||||
export const InterestItem: React.FC<InterestItemProps> = (props) => {
|
||||
export const InterestItem = (props: InterestItemProps): JSX.Element => {
|
||||
const { fontAwesomeIcon, title } = props
|
||||
|
||||
return (
|
||||
|
@ -3,7 +3,7 @@ import { faGit } from '@fortawesome/free-brands-svg-icons'
|
||||
|
||||
import { InterestItem } from './InterestItem'
|
||||
|
||||
export const InterestsList: React.FC = () => {
|
||||
export const InterestsList = (): JSX.Element => {
|
||||
return (
|
||||
<div className='my-4 flex justify-center'>
|
||||
<ul className='m-0 flex w-96 list-none justify-around p-0'>
|
||||
|
@ -1,19 +1,18 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { getI18n } from '@/i18n/i18n.server'
|
||||
|
||||
import type { InterestParagraphProps } from './InterestParagraph'
|
||||
import { InterestParagraph } from './InterestParagraph'
|
||||
import { InterestsList } from './InterestsList'
|
||||
|
||||
export const Interests: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
export const Interests = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
const paragraphs: InterestParagraphProps[] = t(
|
||||
'home:interests.paragraphs',
|
||||
{},
|
||||
{
|
||||
returnObjects: true
|
||||
}
|
||||
let paragraphs = i18n.translate<InterestParagraphProps[]>(
|
||||
'home.interests.paragraphs'
|
||||
)
|
||||
if (!Array.isArray(paragraphs)) {
|
||||
paragraphs = []
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='max-w-full'>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||
import { GitHubIcon } from 'components/Profile/SocialMediaList/SocialMediaIcons/GitHubIcon'
|
||||
import { ShadowContainer } from '@/components/design/ShadowContainer'
|
||||
import { GitHubIcon } from '@/components/Profile/SocialMediaList/SocialMediaIcons/GitHubIcon'
|
||||
|
||||
export interface RepositoryProps {
|
||||
name: string
|
||||
@ -7,7 +7,7 @@ export interface RepositoryProps {
|
||||
href: string
|
||||
}
|
||||
|
||||
export const Repository: React.FC<RepositoryProps> = (props) => {
|
||||
export const Repository = (props: RepositoryProps): JSX.Element => {
|
||||
const { name, description, href } = props
|
||||
|
||||
return (
|
||||
|
@ -1,33 +1,35 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { getI18n } from '@/i18n/i18n.server'
|
||||
|
||||
import { Repository } from './Repository'
|
||||
|
||||
export const OpenSource: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
export const OpenSource = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<div className='mt-0 flex max-w-full flex-col items-center'>
|
||||
<p className='text-center'>{t('home:open-source.description')}</p>
|
||||
<p className='text-center'>
|
||||
{i18n.translate('home.open-source.description')}
|
||||
</p>
|
||||
<div className='my-6 grid grid-cols-1 gap-6 md:w-10/12 md:grid-cols-2'>
|
||||
<Repository
|
||||
name='nodejs/node'
|
||||
description='Node.js JavaScript runtime 🐢🚀'
|
||||
href='https://github.com/nodejs/node/commits?author=Divlo'
|
||||
description='Node.js JavaScript runtime ✨🐢🚀✨'
|
||||
href='https://github.com/nodejs/node/commits?author=theoludwig'
|
||||
/>
|
||||
<Repository
|
||||
name='standard/standard'
|
||||
description='🌟 JavaScript Style Guide, with linter & automatic code fixer'
|
||||
href='https://github.com/standard/standard/commits?author=Divlo'
|
||||
href='https://github.com/standard/standard/commits?author=theoludwig'
|
||||
/>
|
||||
<Repository
|
||||
name='nrwl/nx'
|
||||
description='Smart, Extensible Build Framework'
|
||||
href='https://github.com/nrwl/nx/commits?author=Divlo'
|
||||
description='Smart, Fast and Extensible Build System'
|
||||
href='https://github.com/nrwl/nx/commits?author=theoludwig'
|
||||
/>
|
||||
<Repository
|
||||
name='vercel/next.js'
|
||||
description='The React Framework for Production'
|
||||
href='https://github.com/vercel/next.js/commits?author=Divlo'
|
||||
description='The React Framework'
|
||||
href='https://github.com/vercel/next.js/commits?author=theoludwig'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||
import { ShadowContainer } from '@/components/design/ShadowContainer'
|
||||
|
||||
export interface PortfolioItemProps {
|
||||
title: string
|
||||
@ -9,7 +9,7 @@ export interface PortfolioItemProps {
|
||||
image: string
|
||||
}
|
||||
|
||||
export const PortfolioItem: React.FC<PortfolioItemProps> = (props) => {
|
||||
export const PortfolioItem = (props: PortfolioItemProps): JSX.Element => {
|
||||
const { title, description, link, image } = props
|
||||
|
||||
return (
|
||||
|
@ -1,18 +1,15 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { getI18n } from '@/i18n/i18n.server'
|
||||
|
||||
import type { PortfolioItemProps } from './PortfolioItem'
|
||||
import { PortfolioItem } from './PortfolioItem'
|
||||
|
||||
export const Portfolio: React.FC = () => {
|
||||
const { t } = useTranslation('home')
|
||||
export const Portfolio = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
const items: PortfolioItemProps[] = t(
|
||||
'home:portfolio.items',
|
||||
{},
|
||||
{
|
||||
returnObjects: true
|
||||
let items = i18n.translate<PortfolioItemProps[]>('home.portfolio.items')
|
||||
if (!Array.isArray(items)) {
|
||||
items = []
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='flex w-full flex-wrap justify-center px-3'>
|
||||
|
@ -1,17 +1,17 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { getI18n } from '@/i18n/i18n.server'
|
||||
|
||||
export const ProfileDescriptionBottom: React.FC = () => {
|
||||
const { t, lang } = useTranslation()
|
||||
export const ProfileDescriptionBottom = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<p className='mb-8 mt-8 text-base font-normal text-gray dark:text-gray-dark'>
|
||||
{t('home:about.description-bottom')}
|
||||
{lang === 'fr' ? (
|
||||
{i18n.translate('home.about.description-bottom')}
|
||||
{i18n.locale === 'fr-FR' ? (
|
||||
<>
|
||||
<br />
|
||||
<br />
|
||||
<a
|
||||
href='/curriculum-vitae'
|
||||
href='/curriculum-vitae/index.html'
|
||||
className='text-yellow hover:underline dark:text-yellow-dark'
|
||||
>
|
||||
Curriculum vitæ
|
||||
|
@ -1,14 +1,16 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { getI18n } from '@/i18n/i18n.server'
|
||||
|
||||
export const ProfileInformation: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
export const ProfileInformation = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<div className='mb-6 border-b-2 border-gray-600 pb-2 font-headline dark:border-gray-400'>
|
||||
<h1 className='mb-2 text-4xl font-semibold text-yellow dark:text-yellow-dark'>
|
||||
Théo LUDWIG (Divlo)
|
||||
Théo LUDWIG
|
||||
</h1>
|
||||
<h2 className='mb-3 text-base'>{t('home:about.description')}</h2>
|
||||
<h2 className='mb-3 text-base'>
|
||||
{i18n.translate('home.about.description')}
|
||||
</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ interface ProfileItemProps {
|
||||
link?: string
|
||||
}
|
||||
|
||||
export const ProfileItem: React.FC<ProfileItemProps> = (props) => {
|
||||
export const ProfileItem = (props: ProfileItemProps): JSX.Element => {
|
||||
const { title, value, link } = props
|
||||
|
||||
return (
|
||||
|
@ -1,34 +1,46 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { DIVLO_BIRTH_DATE, DIVLO_BIRTH_DATE_STRING, getAge } from 'utils/getAge'
|
||||
import { useI18n } from '@/i18n/i18n.client'
|
||||
import { BIRTH_DATE, BIRTH_DATE_STRING, getAge } from '@/utils/getAge'
|
||||
import type { CookiesStore } from '@/utils/constants'
|
||||
|
||||
import { ProfileItem } from './ProfileItem'
|
||||
|
||||
export const ProfileList: React.FC = () => {
|
||||
const { t } = useTranslation('home')
|
||||
export interface ProfileListProps {
|
||||
cookiesStore: CookiesStore
|
||||
}
|
||||
|
||||
export const ProfileList = (props: ProfileListProps): JSX.Element => {
|
||||
const { cookiesStore } = props
|
||||
|
||||
const i18n = useI18n(cookiesStore)
|
||||
|
||||
const age = useMemo(() => {
|
||||
return getAge(DIVLO_BIRTH_DATE)
|
||||
return getAge(BIRTH_DATE)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ul className='m-0 list-none p-0'>
|
||||
<ProfileItem
|
||||
title={t('home:about.pronouns')}
|
||||
value={t('home:about.pronouns-value')}
|
||||
title={i18n.translate('home.about.pronouns')}
|
||||
value={i18n.translate('home.about.pronouns-value')}
|
||||
/>
|
||||
<ProfileItem
|
||||
title={t('home:about.birth-date')}
|
||||
value={`${DIVLO_BIRTH_DATE_STRING} (${age} ${t(
|
||||
'home:about.years-old'
|
||||
title={i18n.translate('home.about.birth-date')}
|
||||
value={`${BIRTH_DATE_STRING} (${age} ${i18n.translate(
|
||||
'home.about.years-old'
|
||||
)})`}
|
||||
/>
|
||||
<ProfileItem title={t('home:about.nationality')} value='Alsace, France' />
|
||||
<ProfileItem
|
||||
title={i18n.translate('home.about.nationality')}
|
||||
value='Alsace, France'
|
||||
/>
|
||||
<ProfileItem
|
||||
title='Email'
|
||||
value='contact@divlo.fr'
|
||||
link='mailto:contact@divlo.fr'
|
||||
value='contact@theoludwig.fr'
|
||||
link='mailto:contact@theoludwig.fr'
|
||||
/>
|
||||
</ul>
|
||||
)
|
||||
|
@ -1,11 +1,11 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
import DivloLogo from 'public/images/divlo_logo.png'
|
||||
import Logo from 'public/images/logo.png'
|
||||
|
||||
export const ProfileLogo: React.FC = () => {
|
||||
export const ProfileLogo = (): JSX.Element => {
|
||||
return (
|
||||
<div className='max-h-[370px] max-w-[370px] px-2 py-6'>
|
||||
<Image quality={100} src={DivloLogo} alt='Divlo' priority />
|
||||
<Image quality={100} src={Logo} alt='Théo LUDWIG' priority />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Icon } from './Icon'
|
||||
|
||||
export const EmailIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const EmailIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Email</title>
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Icon } from './Icon'
|
||||
|
||||
export const GitHubIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const GitHubIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>GitHub</title>
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Icon } from './Icon'
|
||||
|
||||
export const GitLabIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const GitLabIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>GitLab</title>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import classNames from 'clsx'
|
||||
|
||||
export const Icon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const Icon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => {
|
||||
const { children, className, ...rest } = props
|
||||
|
||||
return (
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Icon } from './Icon'
|
||||
|
||||
export const NPMIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const NPMIcon = (props: React.SVGProps<SVGSVGElement>): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>npm</title>
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Icon } from './Icon'
|
||||
|
||||
export const TwitchIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const TwitchIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Twitch</title>
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Icon } from './Icon'
|
||||
|
||||
export const TwitterIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const TwitterIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>Twitter</title>
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Icon } from './Icon'
|
||||
|
||||
export const YouTubeIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
|
||||
export const YouTubeIcon = (
|
||||
props: React.SVGProps<SVGSVGElement>
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<Icon {...props}>
|
||||
<title>YouTube</title>
|
||||
|
@ -1,11 +1,9 @@
|
||||
interface SocialMediaItemProps {
|
||||
interface SocialMediaItemProps extends React.PropsWithChildren {
|
||||
link: string
|
||||
ariaLabel: string
|
||||
}
|
||||
|
||||
export const SocialMediaItem: React.FC<
|
||||
React.PropsWithChildren<SocialMediaItemProps>
|
||||
> = (props) => {
|
||||
export const SocialMediaItem = (props: SocialMediaItemProps): JSX.Element => {
|
||||
const { link, ariaLabel, children } = props
|
||||
|
||||
return (
|
||||
|
@ -7,31 +7,37 @@ import { TwitchIcon } from './SocialMediaIcons/TwitchIcon'
|
||||
import { EmailIcon } from './SocialMediaIcons/EmailIcon'
|
||||
import { NPMIcon } from './SocialMediaIcons/NPMIcon'
|
||||
|
||||
export const SocialMediaList: React.FC = () => {
|
||||
export const SocialMediaList = (): JSX.Element => {
|
||||
return (
|
||||
<ul className='social-media-list m-0 mt-2 list-none py-4 text-center'>
|
||||
<SocialMediaItem link='https://github.com/Divlo' ariaLabel='GitHub'>
|
||||
<SocialMediaItem link='https://github.com/theoludwig' ariaLabel='GitHub'>
|
||||
<GitHubIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem link='https://gitlab.com/Divlo' ariaLabel='GitLab'>
|
||||
<SocialMediaItem link='https://gitlab.com/theoludwig' ariaLabel='GitLab'>
|
||||
<GitLabIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem link='https://www.npmjs.com/~divlo' ariaLabel='NPM'>
|
||||
<SocialMediaItem link='https://www.npmjs.com/~theoludwig' ariaLabel='npm'>
|
||||
<NPMIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem link='https://twitter.com/Divlo_FR' ariaLabel='Twitter'>
|
||||
<SocialMediaItem
|
||||
link='https://twitter.com/theoludwig_'
|
||||
ariaLabel='Twitter'
|
||||
>
|
||||
<TwitterIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem
|
||||
link='https://www.youtube.com/c/Divlo'
|
||||
link='https://www.youtube.com/@theo_ludwig'
|
||||
ariaLabel='YouTube'
|
||||
>
|
||||
<YouTubeIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem link='https://www.twitch.tv/divlo' ariaLabel='Twitch'>
|
||||
<SocialMediaItem
|
||||
link='https://www.twitch.tv/theoludwig'
|
||||
ariaLabel='Twitch'
|
||||
>
|
||||
<TwitchIcon />
|
||||
</SocialMediaItem>
|
||||
<SocialMediaItem link='mailto:contact@divlo.fr' ariaLabel='Email'>
|
||||
<SocialMediaItem link='mailto:contact@theoludwig.fr' ariaLabel='Email'>
|
||||
<EmailIcon />
|
||||
</SocialMediaItem>
|
||||
</ul>
|
||||
|
@ -1,15 +1,19 @@
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
import { ProfileDescriptionBottom } from './ProfileDescriptionBottom'
|
||||
import { ProfileInformation } from './ProfileInfo'
|
||||
import { ProfileList } from './ProfileList'
|
||||
import { ProfileLogo } from './ProfileLogo'
|
||||
|
||||
export const Profile: React.FC = () => {
|
||||
export const Profile = (): JSX.Element => {
|
||||
const cookiesStore = cookies()
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center px-10 pt-2 md:flex-row md:pt-10'>
|
||||
<ProfileLogo />
|
||||
<div>
|
||||
<ProfileInformation />
|
||||
<ProfileList />
|
||||
<ProfileList cookiesStore={cookiesStore.toString()} />
|
||||
<ProfileDescriptionBottom />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useTheme } from 'next-themes'
|
||||
import Image from 'next/image'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { getTheme } from '@/theme/theme.server'
|
||||
|
||||
import type { SkillName } from './skills'
|
||||
import { skills } from './skills'
|
||||
@ -9,12 +9,14 @@ export interface SkillComponentProps {
|
||||
skill: SkillName
|
||||
}
|
||||
|
||||
export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
||||
export const SkillComponent = (props: SkillComponentProps): JSX.Element => {
|
||||
const { skill } = props
|
||||
const skillProperties = skills[skill]
|
||||
const { theme } = useTheme()
|
||||
|
||||
const image = useMemo(() => {
|
||||
const skillProperties = skills[skill]
|
||||
|
||||
const theme = getTheme()
|
||||
|
||||
const getImage = (): string => {
|
||||
if (typeof skillProperties.image === 'string') {
|
||||
return skillProperties.image
|
||||
}
|
||||
@ -22,7 +24,7 @@ export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
||||
return skillProperties.image.light
|
||||
}
|
||||
return skillProperties.image.dark
|
||||
}, [skillProperties, theme])
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
@ -33,12 +35,12 @@ export const SkillComponent: React.FC<SkillComponentProps> = (props) => {
|
||||
>
|
||||
<div className='text-center'>
|
||||
<Image
|
||||
className='inline h-auto w-auto'
|
||||
className='inline h-16 w-16'
|
||||
quality={100}
|
||||
width={60}
|
||||
height={60}
|
||||
width={64}
|
||||
height={64}
|
||||
alt={skill}
|
||||
src={image}
|
||||
src={getImage()}
|
||||
/>
|
||||
<p className='mt-1'>{skill}</p>
|
||||
</div>
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { ShadowContainer } from 'components/design/ShadowContainer'
|
||||
import { ShadowContainer } from '@/components/design/ShadowContainer'
|
||||
|
||||
export interface SkillsSectionProps {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const SkillsSection: React.FC<SkillsSectionProps> = (props) => {
|
||||
export const SkillsSection = (props: SkillsSectionProps): JSX.Element => {
|
||||
const { title, children } = props
|
||||
|
||||
return (
|
||||
|
@ -1,15 +1,14 @@
|
||||
import useTranslation from 'next-translate/useTranslation'
|
||||
import { getI18n } from '@/i18n/i18n.server'
|
||||
|
||||
import { SkillComponent } from './Skill'
|
||||
import { SkillsSection } from './SkillsSection'
|
||||
|
||||
export const Skills: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
export const Skills = (): JSX.Element => {
|
||||
const i18n = getI18n()
|
||||
|
||||
return (
|
||||
<>
|
||||
<SkillsSection title={t('home:skills.languages')}>
|
||||
<SkillComponent skill='JavaScript' />
|
||||
<SkillsSection title={i18n.translate('home.skills.languages')}>
|
||||
<SkillComponent skill='TypeScript' />
|
||||
<SkillComponent skill='Python' />
|
||||
<SkillComponent skill='C/C++' />
|
||||
@ -30,9 +29,9 @@ export const Skills: React.FC = () => {
|
||||
<SkillComponent skill='PostgreSQL' />
|
||||
</SkillsSection>
|
||||
|
||||
<SkillsSection title={t('home:skills.software-tools')}>
|
||||
<SkillsSection title={i18n.translate('home.skills.software-tools')}>
|
||||
<SkillComponent skill='GNU/Linux' />
|
||||
<SkillComponent skill='Ubuntu' />
|
||||
<SkillComponent skill='Arch Linux' />
|
||||
<SkillComponent skill='Visual Studio Code' />
|
||||
<SkillComponent skill='Git' />
|
||||
<SkillComponent skill='Docker' />
|
||||
|
@ -88,20 +88,20 @@ export const skills = {
|
||||
},
|
||||
'Visual Studio Code': {
|
||||
link: 'https://code.visualstudio.com/',
|
||||
image: '/images/skills/Visual_Studio_Code.png'
|
||||
image: '/images/skills/VisualStudioCode.png'
|
||||
},
|
||||
Git: {
|
||||
link: 'https://git-scm.com/',
|
||||
image: '/images/skills/Git.png'
|
||||
},
|
||||
Hyper: {
|
||||
link: 'https://hyper.is/',
|
||||
image: '/images/skills/Hyper.svg'
|
||||
},
|
||||
Ubuntu: {
|
||||
link: 'https://ubuntu.com/',
|
||||
image: '/images/skills/Ubuntu.png'
|
||||
},
|
||||
'Arch Linux': {
|
||||
link: 'https://archlinux.org/',
|
||||
image: '/images/skills/ArchLinux.png'
|
||||
},
|
||||
'GNU/Linux': {
|
||||
link: 'https://www.gnu.org/',
|
||||
image: '/images/skills/GNU-Linux.png'
|
||||
|
28
components/design/Loader.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import classNames from 'clsx'
|
||||
|
||||
export interface LoaderProps {
|
||||
width?: number
|
||||
height?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const Loader = (props: LoaderProps): JSX.Element => {
|
||||
const { width = 50, height = 50, className } = props
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width,
|
||||
height
|
||||
}}
|
||||
className={classNames(
|
||||
'animate-spin inline-block border-[3px] border-current border-t-transparent text-yellow dark:text-yellow-dark rounded-full',
|
||||
className
|
||||
)}
|
||||
role='status'
|
||||
aria-label='loading'
|
||||
>
|
||||
<span className='sr-only'>Loading...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,6 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export const RevealFade: React.FC<React.PropsWithChildren> = (props) => {
|
||||
export type RevealFadeProps = React.PropsWithChildren
|
||||
|
||||
export const RevealFade = (props: RevealFadeProps): JSX.Element => {
|
||||
const { children } = props
|
||||
|
||||
const htmlElement = useRef<HTMLDivElement>(null)
|
||||
@ -8,13 +12,13 @@ export const RevealFade: React.FC<React.PropsWithChildren> = (props) => {
|
||||
useEffect(() => {
|
||||
const observer = new window.IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
entries.forEach((entry) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.className =
|
||||
'opacity-100 visible translate-y-0 transition-all duration-700 ease-in-out'
|
||||
observer.unobserve(entry.target)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
root: null,
|
||||
|
@ -1,6 +1,6 @@
|
||||
type SectionHeadingProps = React.ComponentPropsWithRef<'h2'>
|
||||
|
||||
export const SectionHeading: React.FC<SectionHeadingProps> = (props) => {
|
||||
export const SectionHeading = (props: SectionHeadingProps): JSX.Element => {
|
||||
const { children, ...rest } = props
|
||||
|
||||
return (
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ShadowContainer } from '../ShadowContainer'
|
||||
import { SectionHeading } from './SectionHeading'
|
||||
import { ShadowContainer } from '@/components/design/ShadowContainer'
|
||||
import { SectionHeading } from '@/components/design/Section/SectionHeading'
|
||||
|
||||
type SectionProps = React.ComponentPropsWithRef<'section'> & {
|
||||
heading?: string
|
||||
@ -8,7 +8,7 @@ type SectionProps = React.ComponentPropsWithRef<'section'> & {
|
||||
withoutShadowContainer?: boolean
|
||||
}
|
||||
|
||||
export const Section: React.FC<SectionProps> = (props) => {
|
||||
export const Section = (props: SectionProps): JSX.Element => {
|
||||
const {
|
||||
children,
|
||||
heading,
|
||||
|
@ -2,7 +2,7 @@ import classNames from 'clsx'
|
||||
|
||||
type ShadowContainerProps = React.ComponentPropsWithRef<'div'>
|
||||
|
||||
export const ShadowContainer: React.FC<ShadowContainerProps> = (props) => {
|
||||
export const ShadowContainer = (props: ShadowContainerProps): JSX.Element => {
|
||||
const { children, className, ...rest } = props
|
||||
|
||||
return (
|
||||
|
@ -1,11 +1,11 @@
|
||||
services:
|
||||
divlo:
|
||||
theoludwig:
|
||||
container_name: ${COMPOSE_PROJECT_NAME}
|
||||
image: 'divlo'
|
||||
image: 'theoludwig'
|
||||
restart: 'unless-stopped'
|
||||
build:
|
||||
context: './'
|
||||
ports:
|
||||
- '${PORT-3000}:${PORT-3000}'
|
||||
network_mode: 'host'
|
||||
environment:
|
||||
PORT: ${PORT-3000}
|
||||
env_file: '.env'
|
20
curriculum-vitae/build.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { build } from 'vite'
|
||||
|
||||
const curriculumVitae = new URL('./', import.meta.url)
|
||||
const curriculumVitaeDist = new URL('./dist', curriculumVitae)
|
||||
const publicCurriculumVitaeOutputURL = new URL(
|
||||
'../public/curriculum-vitae',
|
||||
import.meta.url
|
||||
)
|
||||
|
||||
await build({
|
||||
root: fileURLToPath(curriculumVitae),
|
||||
base: '/curriculum-vitae/'
|
||||
})
|
||||
|
||||
await fs.promises.cp(curriculumVitaeDist, publicCurriculumVitaeOutputURL, {
|
||||
recursive: true
|
||||
})
|
@ -1,19 +1,16 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json",
|
||||
"meta": {
|
||||
"theme": "custom"
|
||||
},
|
||||
"basics": {
|
||||
"name": "Théo LUDWIG",
|
||||
"label": "Développeur Full Stack • Étudiant",
|
||||
"image": "https://divlo.fr/images/logo_orange.png",
|
||||
"email": "contact@divlo.fr",
|
||||
"image": "https://theoludwig.fr/images/logo_orange.png",
|
||||
"email": "contact@theoludwig.fr",
|
||||
"age": "31/03/2003",
|
||||
"location": {
|
||||
"address": "Alsace, France"
|
||||
},
|
||||
"url": "https://divlo.fr",
|
||||
"summary": "Je suis étudiant à l'université suivant la formation \"BUT Informatique\" et me forme en autodidacte dans l'informatique en suivant des formations en ligne. <br/> Je mets en pratique tout ce que j'apprends et réalise de nombreux projets (disponible sur <a href=\"https://divlo.fr\">divlo.fr</a>)."
|
||||
"url": "https://theoludwig.fr",
|
||||
"summary": "Je suis étudiant à l'université suivant la formation \"BUT Informatique\" et me forme en autodidacte dans l'informatique en suivant des formations en ligne. <br/> Je mets en pratique tout ce que j'apprends et réalise de nombreux projets (disponible sur <a href=\"https://theoludwig.fr\">theoludwig.fr</a>)."
|
||||
},
|
||||
"education": [
|
||||
{
|
||||
@ -66,8 +63,8 @@
|
||||
"location": "4 Rue Sophie Germain, 67720 Hœrdt",
|
||||
"position": "Stagiaire Développeur Web",
|
||||
"startDate": "2023-04-11",
|
||||
"endDate": "2023-06-23",
|
||||
"duration": "3 mois"
|
||||
"endDate": "2023-07-26",
|
||||
"duration": "4 mois"
|
||||
},
|
||||
{
|
||||
"summary": "Agent administratif - Numérisation et archivage des plans électriques initialement sous format papier calque.",
|
||||
@ -105,7 +102,7 @@
|
||||
"website": "https://www.wildcodeschool.fr/",
|
||||
"name": "Wild Code School",
|
||||
"location": "32 Rue du Bass. d'Austerlitz, 67100 Strasbourg",
|
||||
"position": "Initiation métier développeur web",
|
||||
"position": "Initiation métier Développeur web",
|
||||
"startDate": "2019-06-24",
|
||||
"endDate": "2019-06-28",
|
||||
"duration": "1 semaine"
|
||||
@ -136,16 +133,16 @@
|
||||
},
|
||||
{
|
||||
"keywords": ["HTML", "CSS", "Tailwind CSS", "React.js/Next.js"],
|
||||
"name": "Front-end"
|
||||
"name": "Frontend"
|
||||
},
|
||||
{
|
||||
"keywords": ["Laravel", "Node.js", "Fastify", "PostgreSQL"],
|
||||
"name": "Back-end"
|
||||
"name": "Backend"
|
||||
},
|
||||
{
|
||||
"keywords": [
|
||||
"GNU/Linux",
|
||||
"Ubuntu",
|
||||
"Arch Linux",
|
||||
"Visual Studio Code",
|
||||
"Git",
|
||||
"Docker"
|
Before Width: | Height: | Size: 1015 B After Width: | Height: | Size: 1015 B |
Before Width: | Height: | Size: 986 B After Width: | Height: | Size: 986 B |
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 629 B |
Before Width: | Height: | Size: 912 B After Width: | Height: | Size: 912 B |
Before Width: | Height: | Size: 528 B After Width: | Height: | Size: 528 B |
@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="fr-FR">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
@ -1,267 +1,27 @@
|
||||
{
|
||||
"name": "jsonresume-theme-custom",
|
||||
"name": "curriculum-vitae",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "jsonresume-theme-custom",
|
||||
"name": "curriculum-vitae",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"jsonc-parser": "3.2.0",
|
||||
"modern-normalize": "2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.2.5",
|
||||
"date-and-time": "3.0.0",
|
||||
"vite": "4.3.9",
|
||||
"@types/node": "20.4.5",
|
||||
"date-and-time": "3.0.2",
|
||||
"vite": "4.4.7",
|
||||
"vite-plugin-html": "3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
|
||||
"integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
|
||||
"integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
|
||||
"integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
|
||||
"integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
|
||||
"integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
|
||||
"integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
|
||||
"integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
|
||||
"integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
|
||||
"integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
|
||||
"integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
|
||||
"integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
|
||||
"integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
|
||||
"integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
|
||||
"integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
|
||||
"integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
|
||||
"integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@ -274,102 +34,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
|
||||
"integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
|
||||
"integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
|
||||
"integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
|
||||
"integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
|
||||
"integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
|
||||
"integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
|
||||
@ -403,9 +67,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz",
|
||||
"integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==",
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
|
||||
"integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.0",
|
||||
@ -483,15 +147,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz",
|
||||
"integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==",
|
||||
"version": "20.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz",
|
||||
"integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.8.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
|
||||
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
|
||||
"integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@ -682,9 +346,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/date-and-time": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.0.0.tgz",
|
||||
"integrity": "sha512-uuzXp/mvv6jEMLiP5QzERSQPzHqYnv9i8NZ8BS5kYeB2sakv74EewQiCS4Ahxwq3In+9fYZhGztuDHRVzIOkFQ==",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.0.2.tgz",
|
||||
"integrity": "sha512-MOqlRertOQmQI7ySbz6dKLM7Rxm9dgcPuBI9IL7NVe0UGqHPK+6hWSKVhLrVHxlSgQQtocE2R7+HFOf5aMz8vw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
@ -753,12 +417,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
|
||||
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
|
||||
"version": "16.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
|
||||
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/motdotla/dotenv?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv-expand": {
|
||||
@ -795,9 +462,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
|
||||
"integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.17.tgz",
|
||||
"integrity": "sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
@ -807,28 +474,28 @@
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.17.19",
|
||||
"@esbuild/android-arm64": "0.17.19",
|
||||
"@esbuild/android-x64": "0.17.19",
|
||||
"@esbuild/darwin-arm64": "0.17.19",
|
||||
"@esbuild/darwin-x64": "0.17.19",
|
||||
"@esbuild/freebsd-arm64": "0.17.19",
|
||||
"@esbuild/freebsd-x64": "0.17.19",
|
||||
"@esbuild/linux-arm": "0.17.19",
|
||||
"@esbuild/linux-arm64": "0.17.19",
|
||||
"@esbuild/linux-ia32": "0.17.19",
|
||||
"@esbuild/linux-loong64": "0.17.19",
|
||||
"@esbuild/linux-mips64el": "0.17.19",
|
||||
"@esbuild/linux-ppc64": "0.17.19",
|
||||
"@esbuild/linux-riscv64": "0.17.19",
|
||||
"@esbuild/linux-s390x": "0.17.19",
|
||||
"@esbuild/linux-x64": "0.17.19",
|
||||
"@esbuild/netbsd-x64": "0.17.19",
|
||||
"@esbuild/openbsd-x64": "0.17.19",
|
||||
"@esbuild/sunos-x64": "0.17.19",
|
||||
"@esbuild/win32-arm64": "0.17.19",
|
||||
"@esbuild/win32-ia32": "0.17.19",
|
||||
"@esbuild/win32-x64": "0.17.19"
|
||||
"@esbuild/android-arm": "0.18.17",
|
||||
"@esbuild/android-arm64": "0.18.17",
|
||||
"@esbuild/android-x64": "0.18.17",
|
||||
"@esbuild/darwin-arm64": "0.18.17",
|
||||
"@esbuild/darwin-x64": "0.18.17",
|
||||
"@esbuild/freebsd-arm64": "0.18.17",
|
||||
"@esbuild/freebsd-x64": "0.18.17",
|
||||
"@esbuild/linux-arm": "0.18.17",
|
||||
"@esbuild/linux-arm64": "0.18.17",
|
||||
"@esbuild/linux-ia32": "0.18.17",
|
||||
"@esbuild/linux-loong64": "0.18.17",
|
||||
"@esbuild/linux-mips64el": "0.18.17",
|
||||
"@esbuild/linux-ppc64": "0.18.17",
|
||||
"@esbuild/linux-riscv64": "0.18.17",
|
||||
"@esbuild/linux-s390x": "0.18.17",
|
||||
"@esbuild/linux-x64": "0.18.17",
|
||||
"@esbuild/netbsd-x64": "0.18.17",
|
||||
"@esbuild/openbsd-x64": "0.18.17",
|
||||
"@esbuild/sunos-x64": "0.18.17",
|
||||
"@esbuild/win32-arm64": "0.18.17",
|
||||
"@esbuild/win32-ia32": "0.18.17",
|
||||
"@esbuild/win32-x64": "0.18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
@ -838,9 +505,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.2.12",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
|
||||
"integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
|
||||
"integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
@ -918,20 +585,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
@ -1203,9 +856,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.24",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
|
||||
"integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
|
||||
"version": "8.4.27",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz",
|
||||
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -1270,9 +923,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "3.23.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.23.0.tgz",
|
||||
"integrity": "sha512-h31UlwEi7FHihLe1zbk+3Q7z1k/84rb9BSwmBSr/XjOCEaBJ2YyedQDuM0t/kfOS0IxM+vk1/zI9XxYj9V+NJQ==",
|
||||
"version": "3.26.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.26.3.tgz",
|
||||
"integrity": "sha512-7Tin0C8l86TkpcMtXvQu6saWH93nhG3dGQ1/+l5V2TDMceTxO7kDiK6GzbfLWNNxqJXm591PcEZUozZm51ogwQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
@ -1349,13 +1002,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.17.6",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.17.6.tgz",
|
||||
"integrity": "sha512-V8QHcs8YuyLkLHsJO5ucyff1ykrLVsR4dNnS//L5Y3NiSXpbK1J+WMVUs67eI0KTxs9JtHhgEQpXQVHlHI92DQ==",
|
||||
"version": "5.19.2",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz",
|
||||
"integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.2",
|
||||
"acorn": "^8.5.0",
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.8.2",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
@ -1385,9 +1038,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz",
|
||||
"integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==",
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz",
|
||||
"integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
@ -1400,14 +1053,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.3.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
|
||||
"integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
|
||||
"version": "4.4.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz",
|
||||
"integrity": "sha512-6pYf9QJ1mHylfVh39HpuSfMPojPSKVxZvnclX1K1FyZ1PXDOcLBibdq5t1qxJSnL63ca8Wf4zts6mD8u8oc9Fw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.17.5",
|
||||
"postcss": "^8.4.23",
|
||||
"rollup": "^3.21.0"
|
||||
"esbuild": "^0.18.10",
|
||||
"postcss": "^8.4.26",
|
||||
"rollup": "^3.25.2"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@ -1415,12 +1068,16 @@
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">= 14",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
@ -1433,6 +1090,9 @@
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "jsonresume-theme-custom",
|
||||
"name": "curriculum-vitae",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
@ -13,9 +13,9 @@
|
||||
"modern-normalize": "2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.2.5",
|
||||
"date-and-time": "3.0.0",
|
||||
"vite": "4.3.9",
|
||||
"@types/node": "20.4.5",
|
||||
"date-and-time": "3.0.2",
|
||||
"vite": "4.4.7",
|
||||
"vite-plugin-html": "3.2.0"
|
||||
}
|
||||
}
|
5
curriculum-vitae/scripts/main.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { BIRTH_DATE, getAge } from '../../utils/getAge.ts'
|
||||
|
||||
const yearOld = document.getElementById('year-old')
|
||||
|
||||
yearOld.textContent = getAge(BIRTH_DATE).toString()
|