chore: separate backend from frontend
Some checks failed
Publish Docker Images / build-and-publish (push) Has been cancelled

This commit is contained in:
2025-11-15 12:46:24 +01:00
parent 9f1d4db0de
commit e5f94b9f8e
86 changed files with 134 additions and 14729 deletions

View File

@@ -1 +0,0 @@
/home/phundrak/code/web/phundrak.com

View File

@@ -7,6 +7,3 @@ APP__EMAIL__USER="username"
APP__EMAIL__PASSWORD="changeme"
APP__EMAIL__RECIPIENT="Recipient <user@example.com>"
APP__EMAIL__FROM="Contact Form <noreply@example.com>"
NUXT_PUBLIC_BACKEND_URL=http://localhost:3100
NUXT_PUBLIC_TURNSTILE_SITE_KEY="changeme"
NUXT_TURNSTILE_SECRET_KEY="changeme"

36
.envrc
View File

@@ -12,43 +12,13 @@ dotenv_if_exists
watch_file flake.nix
watch_file flake.lock
watch_file .envrc.local
watch_file backend/shell.nix
watch_file frontend/shell.nix
watch_file nix/shell.nix
# Check if .envrc.local exists and contains a shell preference
if [[ -f .envrc.local ]]; then
source .envrc.local
fi
# If no shell is specified, prompt the user interactively
if [[ -z "$NIX_SHELL_NAME" ]]; then
echo ""
echo "🔧 Available development shells:"
echo " 1) frontend - Nuxt.js/Vue development environment"
echo " 2) backend - Rust backend development environment"
echo ""
echo "💡 Tip: Create a .envrc.local file with 'export NIX_SHELL_NAME=frontend' to skip this prompt"
echo ""
# Read user input
read -p "Select shell (1 or 2): " choice
case $choice in
1|frontend)
NIX_SHELL_NAME=frontend
;;
2|backend)
NIX_SHELL_NAME=backend
;;
*)
echo "❌ Invalid choice. Please select 1 or 2."
return 1
;;
esac
echo "✅ Loading ${NIX_SHELL_NAME} environment..."
fi
if ! use flake ".#${NIX_SHELL_NAME}" --no-pure-eval; then
echo "❌ devenv could not be built. The devenv environment was not loaded. Make the necessary changes to flake.nix and hit enter to try again." >&2
if ! use flake . --no-pure-eval; then
echo "Devenv could not be built. The devenv environment was not loaded. Make the necessary changes to flake.nix and hit enter to try again." >&2
fi

View File

View File

@@ -1,76 +0,0 @@
#+title: phundrak.com
#+html: <a href="https://www.rust-lang.org/"><img src="https://img.shields.io/badge/Rust-Backend-orange.svg?style=flat-square&logo=Rust&logoColor=white" /></a>
#+html: <a href="https://nuxt.com/"><img src="https://img.shields.io/badge/Frontend-Nuxt%204-00DC82?logo=Nuxt.js&logoColor=white&style=flat-square"/></a>
#+html: <a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3-42B883?logo=Vue.js&logoColor=white&style=flat-square"/></a>
#+html: <a href="https://phundrak.com"><img src="https://img.shields.io/badge/Website-phundrak.com-blue?style=flat-square&logo=buffer" /></a>
* Introduction
This is the repository for my website [[https://phundrak.com][phundrak.com]] which contains the
code available on the =main= branch. Code available on the =develop=
branch is available at [[https://beta.phundrak.com][beta.phundrak.com]].
* Architecture
The website follows a modern full-stack architecture:
- *Backend*: Rust using the [[https://github.com/poem-web/poem][Poem]] web framework (located in [[file:backend/][backend/]])
- *Frontend*: Nuxt 4 + Vue 3 + TypeScript (located in [[file:frontend/][frontend/]])
** Backend
The backend is written in Rust and provides a RESTful API using the
Poem framework with OpenAPI support.
*** Running the Backend
To run the backend in development mode:
#+begin_src shell
cd backend
cargo run
#+end_src
To run tests:
#+begin_src shell
cd backend
cargo test
#+end_src
For continuous testing and linting during development, use [[https://dystroy.org/bacon/][bacon]]:
#+begin_src shell
cd backend
bacon
#+end_src
*** Building the Backend
To build the backend for production:
#+begin_src shell
cd backend
cargo build --release
#+end_src
The compiled binary will be available at =backend/target/release/backend=.
** Frontend
The frontend is built with Nuxt 4, Vue 3, and TypeScript, providing a
modern single-page application experience.
*** Installing Dependencies
First, install the required dependencies using =pnpm=:
#+begin_src shell
cd frontend
pnpm install
#+end_src
*** Running the Frontend
To run the frontend in development mode:
#+begin_src shell
cd frontend
pnpm dev
#+end_src
*** Building the Frontend
To build the frontend for production:
#+begin_src shell
cd frontend
pnpm build
#+end_src
The compiled version of the website can then be found in =frontend/.output=.

View File

@@ -1,75 +0,0 @@
{
inputs,
pkgs,
system,
self,
rust-overlay,
...
}: let
rustPlatform = import ./rust-version.nix { inherit rust-overlay inputs system; };
in
inputs.devenv.lib.mkShell {
inherit inputs pkgs;
modules = [
{
devenv.root = let
devenvRootFileContent = builtins.readFile "${self}/.devenv-root";
in
pkgs.lib.mkIf (devenvRootFileContent != "") devenvRootFileContent;
}
{
packages = with rustPlatform.pkgs; [
(rustPlatform.version.override {
extensions = [
"clippy"
"rust-src"
"rust-analyzer"
"rustfmt"
];
})
bacon
cargo-deny
cargo-shuttle
cargo-tarpaulin
cargo-watch
flyctl
just
marksman
tombi # TOML lsp server
];
services.mailpit = {
enable = true;
# HTTP interface for viewing emails
uiListenAddress = "127.0.0.1:8025";
# SMTP server for receiving emails
smtpListenAddress = "127.0.0.1:1025";
};
processes.run.exec = "cargo watch -x run";
enterShell = ''
echo "🦀 Rust backend development environment loaded!"
echo "📦 Rust version: $(rustc --version)"
echo "📦 Cargo version: $(cargo --version)"
echo ""
echo "Available tools:"
echo " - rust-analyzer (LSP)"
echo " - clippy (linter)"
echo " - rustfmt (formatter)"
echo " - bacon (continuous testing/linting)"
echo " - cargo-deny (dependency checker)"
echo " - cargo-tarpaulin (code coverage)"
echo ""
echo "📧 Mailpit service:"
echo " - SMTP server: 127.0.0.1:1025"
echo " - Web UI: http://127.0.0.1:8025"
echo ""
echo "🚀 Quick start:"
echo " Run 'devenv up' to launch:"
echo " - Mailpit service (email testing)"
echo " - Backend with 'cargo watch -x run' (auto-reload)"
'';
}
];
}

46
flake.lock generated
View File

@@ -68,11 +68,11 @@
]
},
"locked": {
"lastModified": 1761922975,
"narHash": "sha256-j4EB5ku/gDm7h7W7A+k70RYj5nUiW/l9wQtXMJUD2hg=",
"lastModified": 1763136231,
"narHash": "sha256-QVtIjPSQ/xVhuXSSENYOYZPfrjjc/W/djuxcJyKxGTw=",
"owner": "cachix",
"repo": "devenv",
"rev": "c9f0b47815a4895fadac87812de8a4de27e0ace1",
"rev": "4b8c2bbdb4e01ef8c4093ee1224fe21ed5ea1a5e",
"type": "github"
},
"original": {
@@ -81,6 +81,18 @@
"type": "github"
}
},
"devenv-root": {
"flake": false,
"locked": {
"narHash": "sha256-d6xi4mKdjkX2JFicDIv5niSzpyI0m/Hnm8GGAIU04kY=",
"type": "file",
"url": "file:///dev/null"
},
"original": {
"type": "file",
"url": "file:///dev/null"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
@@ -140,6 +152,23 @@
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"id": "flake-utils",
"type": "indirect"
}
},
"flakeCompat": {
"flake": false,
"locked": {
@@ -264,9 +293,10 @@
"inputs": {
"alejandra": "alejandra",
"devenv": "devenv",
"devenv-root": "devenv-root",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay",
"systems": "systems"
"rust-overlay": "rust-overlay"
}
},
"rust-analyzer-src": {
@@ -293,11 +323,11 @@
]
},
"locked": {
"lastModified": 1762223900,
"narHash": "sha256-caxpESVH71mdrdihYvQZ9rTZPZqW0GyEG9un7MgpyRM=",
"lastModified": 1763174172,
"narHash": "sha256-u6dcvXk2K6eYVYhmfiN3xmhIf3yUo5KPwm79UOD37Jo=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "cfe1598d69a42a5edb204770e71b8df77efef2c3",
"rev": "89af6762b01409edbb595888a69311e8e5954110",
"type": "github"
},
"original": {

View File

@@ -1,7 +1,6 @@
{
inputs = {
nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling";
systems.url = "github:nix-systems/default";
alejandra = {
url = "github:kamadorueda/alejandra/4.0.0";
inputs.nixpkgs.follows = "nixpkgs";
@@ -14,6 +13,10 @@
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
devenv-root = {
url = "file+file:///dev/null";
flake = false;
};
};
nixConfig = {
@@ -30,27 +33,26 @@
outputs = {
self,
nixpkgs,
devenv,
systems,
flake-utils,
rust-overlay,
alejandra,
...
} @ inputs: let
forEachSystem = nixpkgs.lib.genAttrs (import systems);
in {
formatter = forEachSystem (system: alejandra.defaultPackage.${system});
packages = forEachSystem (system: import ./backend/nix/package.nix { inherit rust-overlay inputs system; });
devShells = forEachSystem (
} @ inputs:
flake-utils.lib.eachDefaultSystem (
system: let
pkgs = nixpkgs.legacyPackages.${system};
in {
backend = import ./backend/nix/shell.nix {
inherit inputs pkgs system self rust-overlay;
overlays = [(import rust-overlay)];
pkgs = import nixpkgs {inherit system overlays;};
rustVersion = pkgs.rust-bin.stable.latest.default;
rustPlatform = pkgs.makeRustPlatform {
cargo = rustVersion;
rustc = rustVersion;
};
frontend = import ./frontend/shell.nix {
inherit inputs pkgs self;
in {
formatter = alejandra.defaultPackage.${system};
packages = import ./nix/package.nix {inherit pkgs rustPlatform;};
devShell = import ./nix/shell.nix {
inherit inputs pkgs self rustVersion;
};
}
);
};
}

View File

@@ -1,17 +0,0 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf",
"experimentalOperatorPosition": "start",
"experimentalTernaries": true,
"jsxSingleQuote": true,
"printWidth": 120,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false
}

View File

@@ -1,134 +0,0 @@
#+title: phundrak.com frontend
#+author: Lucien Cartier-Tilet
#+email: lucien@phundrak.com
This is the frontend of =phundrak.com=, written with Nuxt.
* Setup
** Environment
*** Nix Environment
If you use Nix, you can set up your environment using the [[file:flake.nix][=flake.nix=]]
file, which will give you the exact same development environment as I
use.
#+begin_src bash
nix develop
#+end_src
If you have [[https://direnv.net/][=direnv=]] installed, you can simply use it to automatically
enable this environment. However, I *strongly* recommend you to read the
content of the =flake.nix= file before doing so, as you should with any
Nix-defined environment you did not create.
#+begin_src bash
direnv allow .
#+end_src
*** Required Tools
To be able to work on this project, you need a Javascript package
manager, such as:
- =npm=
- =pnpm= (recommended)
- =yarn=
- =bun=
In my case, I use pnpm.
You can skip this if you are already using my Nix environment.
** Dependencies
Once you have your environment ready, you can now install the
projects dependencies.
#+begin_src bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
#+end_src
* Running the Project
You are now ready to start the development server on
=http://localhost:3000=.
#+begin_src bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
#+end_src
* Production
Once you are satisfied with the project, you can build the application in production mode.
#+begin_src bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
#+end_src
You can preview locally the production build too.
#+begin_src bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
#+end_src
Check out the [[https://nuxt.com/docs/getting-started/deployment][deployment documentation]] for more information.
* Known Issues
** =better-sqlite3= self-registration error
If you encounter an error stating that =better-sqlite3= does not
self-register when running =pnpm run dev=, this is typically caused by
the native module being compiled for a different Node.js version.
*Solution:* Rebuild the native module for your current Node.js version:
#+begin_src bash
# Rebuild just better-sqlite3
pnpm rebuild better-sqlite3
# Or rebuild all native modules
pnpm rebuild
# Or reinstall everything (nuclear option)
rm -rf node_modules
pnpm install
#+end_src
*Why this happens:* =better-sqlite3= contains native C++ code that
needs to be compiled for each specific Node.js version. When you
update Node.js or switch between versions, native modules need to be
rebuilt.

View File

@@ -1,23 +0,0 @@
<template>
<UApp :locale="locales[locale]">
<AppNavbar />
<UMain>
<NuxtPage />
</UMain>
<AppFooter />
</UApp>
</template>
<script setup lang="ts">
import * as locales from '@nuxt/ui/locale';
const { locale } = useI18n();
const lang = computed(() => locales[locale.value].code);
const dir = computed(() => locales[locale.value].dir);
useHead({
htmlAttrs: {
dir,
lang,
},
});
</script>

View File

@@ -1,132 +0,0 @@
:root {
--text-50: oklch(96.68% 0.005 95.1);
--text-100: oklch(93.31% 0.012 96.43);
--text-200: oklch(86.46% 0.023 98.68);
--text-300: oklch(79.55% 0.036 98.17);
--text-400: oklch(72.45% 0.047 99.12);
--text-500: oklch(65.27% 0.06 98.88);
--text-600: oklch(55.54% 0.05 99.33);
--text-700: oklch(45.43% 0.04 98.55);
--text-800: oklch(34.63% 0.028 99.26);
--text-900: oklch(22.99% 0.017 97.01);
--text: oklch(17.69% 0.01 97.92);
--text-950: oklch(16.34% 0.008 95.54);
--background: oklch(97.33% 0.007 88.64);
--background-50: oklch(96.7% 0.008 91.48);
--background-100: oklch(93.46% 0.017 88);
--background-200: oklch(86.85% 0.034 88.07);
--background-300: oklch(80.17% 0.051 88.07);
--background-400: oklch(73.62% 0.069 89.26);
--background-500: oklch(66.8% 0.085 88.59);
--background-600: oklch(56.88% 0.071 88.9);
--background-700: oklch(46.26% 0.056 87.6);
--background-800: oklch(35.24% 0.04 87.71);
--background-900: oklch(23.27% 0.023 87.9);
--background-950: oklch(16.86% 0.012 91.89);
--primary-50: oklch(97.22% 0.012 96.42);
--primary-100: oklch(94.41% 0.025 97.12);
--primary-200: oklch(88.75% 0.05 98.42);
--primary-300: oklch(83.15% 0.074 98.36);
--primary-400: oklch(77.55% 0.097 98.29);
--primary: oklch(74.12% 0.109 98.34);
--primary-500: oklch(72% 0.116 97.93);
--primary-600: oklch(61.14% 0.097 98.09);
--primary-700: oklch(49.77% 0.077 98.34);
--primary-800: oklch(37.71% 0.055 98.79);
--primary-900: oklch(24.68% 0.033 97.74);
--primary-950: oklch(17.23% 0.018 97.53);
--secondary-50: oklch(97.69% 0.019 100.12);
--secondary-100: oklch(95.28% 0.036 96.71);
--secondary-200: oklch(90.57% 0.07 97.74);
--secondary-300: oklch(86.23% 0.103 98.42);
--secondary: oklch(83.86% 0.116 98.04);
--secondary-400: oklch(81.72% 0.129 98.31);
--secondary-500: oklch(77.44% 0.146 97.07);
--secondary-600: oklch(65.69% 0.123 97.5);
--secondary-700: oklch(53.48% 0.099 97.52);
--secondary-800: oklch(40.18% 0.072 97.19);
--secondary-900: oklch(26.04% 0.043 96.76);
--secondary-950: oklch(18.17% 0.026 97.52);
--accent-50: oklch(97.77% 0.019 96.86);
--accent-100: oklch(95.53% 0.039 97.44);
--accent-200: oklch(91.16% 0.076 97.81);
--accent-300: oklch(86.92% 0.11 97.94);
--accent: oklch(82.74% 0.136 98);
--accent-400: oklch(82.74% 0.136 98);
--accent-500: oklch(78.81% 0.152 96.76);
--accent-600: oklch(66.8% 0.128 96.97);
--accent-700: oklch(54.33% 0.103 96.65);
--accent-800: oklch(40.98% 0.076 96.95);
--accent-900: oklch(26.42% 0.045 97.53);
--accent-950: oklch(18.44% 0.029 102.49);
}
.dark {
--text-50: oklch(16.34% 0.008 95.54);
--text: oklch(96.05% 0.007 97.35);
--text-100: oklch(22.99% 0.017 97.01);
--text-200: oklch(34.63% 0.028 99.26);
--text-300: oklch(45.43% 0.04 98.55);
--text-400: oklch(55.54% 0.05 99.33);
--text-500: oklch(65.27% 0.06 98.88);
--text-600: oklch(72.45% 0.047 99.12);
--text-700: oklch(79.55% 0.036 98.17);
--text-800: oklch(86.46% 0.023 98.68);
--text-900: oklch(93.31% 0.012 96.43);
--text-950: oklch(96.68% 0.005 95.1);
--background-50: oklch(16.86% 0.012 91.89);
--background-100: oklch(23.27% 0.023 87.9);
--background-200: oklch(35.24% 0.04 87.71);
--background-300: oklch(46.26% 0.056 87.6);
--background-400: oklch(56.88% 0.071 88.9);
--background-500: oklch(66.8% 0.085 88.59);
--background-600: oklch(73.62% 0.069 89.26);
--background-700: oklch(80.17% 0.051 88.07);
--background-800: oklch(86.85% 0.034 88.07);
--background-900: oklch(93.46% 0.017 88);
--background-950: oklch(96.7% 0.008 91.48);
--background: oklch(15.48% 0.011 89.86);
--primary-50: oklch(17.23% 0.018 97.53);
--primary-100: oklch(24.68% 0.033 97.74);
--primary-200: oklch(37.71% 0.055 98.79);
--primary-300: oklch(49.77% 0.077 98.34);
--primary-400: oklch(61.14% 0.097 98.09);
--primary: oklch(67.74% 0.108 98.2);
--primary-500: oklch(72% 0.116 97.93);
--primary-600: oklch(77.55% 0.097 98.29);
--primary-700: oklch(83.15% 0.074 98.36);
--primary-800: oklch(88.75% 0.05 98.42);
--primary-900: oklch(94.41% 0.025 97.12);
--primary-950: oklch(97.22% 0.012 96.42);
--secondary-50: oklch(18.17% 0.026 97.52);
--secondary-100: oklch(26.04% 0.043 96.76);
--secondary-200: oklch(40.18% 0.072 97.19);
--secondary-300: oklch(53.48% 0.099 97.52);
--secondary: oklch(59.61% 0.111 97.84);
--secondary-400: oklch(65.69% 0.123 97.5);
--secondary-500: oklch(77.44% 0.146 97.07);
--secondary-600: oklch(81.72% 0.129 98.31);
--secondary-700: oklch(86.23% 0.103 98.42);
--secondary-800: oklch(90.57% 0.07 97.74);
--secondary-900: oklch(95.28% 0.036 96.71);
--secondary-950: oklch(97.69% 0.019 100.12);
--accent-50: oklch(18.44% 0.029 102.49);
--accent-100: oklch(26.42% 0.045 97.53);
--accent-200: oklch(40.98% 0.076 96.95);
--accent-300: oklch(54.33% 0.103 96.65);
--accent: oklch(66.8% 0.128 96.97);
--accent-400: oklch(66.8% 0.128 96.97);
--accent-500: oklch(78.81% 0.152 96.76);
--accent-600: oklch(82.74% 0.136 98);
--accent-700: oklch(86.92% 0.11 97.94);
--accent-800: oklch(91.16% 0.076 97.81);
--accent-900: oklch(95.53% 0.039 97.44);
--accent-950: oklch(97.77% 0.019 96.86);
}

View File

@@ -1,6 +0,0 @@
@import '@nuxt/ui';
@import './colors.css';
@import './ui/index.css';
@import './tailwind.css';
@source "../../../content/**/*";

View File

@@ -1,85 +0,0 @@
@import 'tailwindcss';
@theme {
--color-text-50: var(--text-50);
--color-text: var(--text);
--color-text-100: var(--text-100);
--color-text-200: var(--text-200);
--color-text-300: var(--text-300);
--color-text-400: var(--text-400);
--color-text-500: var(--text-500);
--color-text-600: var(--text-600);
--color-text-700: var(--text-700);
--color-text-800: var(--text-800);
--color-text-900: var(--text-900);
--color-text-950: var(--text-950);
--color-background-50: var(--background-50);
--color-background-100: var(--background-100);
--color-background-200: var(--background-200);
--color-background-300: var(--background-300);
--color-background-400: var(--background-400);
--color-background-500: var(--background-500);
--color-background-600: var(--background-600);
--color-background-700: var(--background-700);
--color-background-800: var(--background-800);
--color-background-900: var(--background-900);
--color-background-950: var(--background-950);
--color-background: var(--background);
--color-primary-50: var(--primary-50);
--color-primary-100: var(--primary-100);
--color-primary-200: var(--primary-200);
--color-primary-300: var(--primary-300);
--color-primary-400: var(--primary-400);
--color-primary: var(--primary);
--color-primary-500: var(--primary-500);
--color-primary-600: var(--primary-600);
--color-primary-700: var(--primary-700);
--color-primary-800: var(--primary-800);
--color-primary-900: var(--primary-900);
--color-primary-950: var(--primary-950);
--color-secondary-50: var(--secondary-50);
--color-secondary-100: var(--secondary-100);
--color-secondary-200: var(--secondary-200);
--color-secondary-300: var(--secondary-300);
--color-secondary: var(--secondary);
--color-secondary-400: var(--secondary-400);
--color-secondary-500: var(--secondary-500);
--color-secondary-600: var(--secondary-600);
--color-secondary-700: var(--secondary-700);
--color-secondary-800: var(--secondary-800);
--color-secondary-900: var(--secondary-900);
--color-secondary-950: var(--secondary-950);
--color-accent-50: var(--accent-50);
--color-accent-100: var(--accent-100);
--color-accent-200: var(--accent-200);
--color-accent-300: var(--accent-300);
--color-accent: var(--accent);
--color-accent-400: var(--accent-400);
--color-accent-500: var(--accent-500);
--color-accent-600: var(--accent-600);
--color-accent-700: var(--accent-700);
--color-accent-800: var(--accent-800);
--color-accent-900: var(--accent-900);
--color-accent-950: var(--accent-950);
--text-sm: 0.75rem;
--text-base: 1rem;
--text-xl: 1.333rem;
--text-2xl: 1.777rem;
--text-3xl: 2.369rem;
--text-4xl: 3.158rem;
--text-5xl: 4.21rem;
--text-weight-normal: 400;
--text-weight-bold: 700;
--font-sans:
Noto Sans, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--font-title:
Wittgenstein, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
}

View File

@@ -1,14 +0,0 @@
:root {
--ui-bg: var(--background);
--ui-bg-muted: var(--background-300);
--ui-bg-elevated: var(--background-100);
--ui-bg-accented: var(--backgsound-200);
--ui-bg-inverted: var(--background-900);
}
.dark {
--ui-bg: var(--background);
--ui-bg-muted: var(--background-100);
--ui-bg-elevated: var(--background-200);
--ui-bg-accented: var(--background-300);
--ui-bg-inverted: var(--background-900);
}

View File

@@ -1,12 +0,0 @@
:root {
--ui-border: var(--background-200);
--ui-border-muted: var(--background-200);
--ui-border-accented: var(--background-300);
--ui-border-inverted: var(--background-900);
}
.dark {
--ui-border: var(--background-100);
--ui-border-muted: var(--background-200);
--ui-border-accented: var(--background-200);
--ui-border-inverted: var(--background-900);
}

View File

@@ -1,16 +0,0 @@
:root {
--ui-primary: var(--primary);
--ui-secondary: var(--secondary);
--ui-success: var(--accent);
--ui-info: var(--ui-color-info-500);
--ui-warning: var(--ui-color-warning-500);
--ui-error: var(--ui-color-error-500);
}
.dark {
--ui-primary: var(--primary);
--ui-secondary: var(--secondary);
--ui-success: var(--accent);
--ui-info: var(--ui-color-info-400);
--ui-warning: var(--ui-color-warning-400);
--ui-error: var(--ui-color-error-400);
}

View File

@@ -1,4 +0,0 @@
@import './colors.css';
@import './text.css';
@import './background.css';
@import './border.css';

View File

@@ -1,16 +0,0 @@
:root {
--ui-text-dimmed: var(--text-800);
--ui-text-muted: var(--text-700);
--ui-text-toned: var(--text-600);
--ui-text: var(--text);
--ui-text-highlighted: var(--text-900);
--ui-text-inverted: var(--text-50);
}
.dark {
--ui-text-dimmed: var(--text-800);
--ui-text-muted: var(--text-700);
--ui-text-toned: var(--text-600);
--ui-text: var(--text);
--ui-text-highlighted: var(--text);
--ui-text-inverted: var(--text-50);
}

View File

@@ -1,48 +0,0 @@
<template>
<UFooter class="bg-background-200">
<template #left>
<div class="flex flex-col gap-2">
<p class="text-text-800 text-sm">Copyright &copy; {{ new Date().getFullYear() }}</p>
<p class="text-text-800 text-sm">{{ $t('footer.versions.frontend') }}: {{ version }}</p>
<p class="text-text-800 text-sm">{{ $t('footer.versions.backend') }}: {{ meta?.version }}</p>
</div>
</template>
<UNavigationMenu :items="items" variant="link" :orientation="orientation" />
<template #right>
<UButton
icon="i-simple-icons-github"
color="neutral"
variant="ghost"
to="https://github.com/Phundrak"
target="_blank"
aria-label="GitHub"
/>
</template>
</UFooter>
</template>
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui';
import { version } from '../../package.json';
const { isMobile } = useDevice();
const orientation = computed(() => (isMobile ? 'vertical' : 'horizontal'));
const { getMeta } = useBackend();
const meta = await getMeta();
const items = computed<NavigationMenuItem[]>(() => [
{
label: $t('footer.links.source'),
to: 'https://labs.phundrak.com/phundrak/phundrak.com',
},
{
label: $t('footer.links.nuxt'),
to: 'https://nuxt.com/',
},
{
label: $t('footer.links.rust'),
to: 'https://rust-lang.org/',
},
]);
</script>

View File

@@ -1,29 +0,0 @@
<template>
<UHeader toggle-side="right" mode="drawer">
<template #title> Phundrak </template>
<UNavigationMenu :items="items" />
<template #right>
<NavbarLanguageSwitcher />
<NavbarThemeSwitcher />
</template>
<template #body>
<UNavigationMenu :items="items" orientation="vertical" class="-mx-2.5" />
</template>
</UHeader>
</template>
<script setup lang="ts">
const route = useRoute();
const items = computed<NavigationMenuItem[]>(() => [
{
label: $t('pages.home.name'),
to: '/',
active: route.path == '/',
},
...['resume', 'vocal-synthesis', 'languages', 'contact'].map((page) => ({
label: $t(`pages.${page}.name`),
to: `/${page}`,
active: route.path.startsWith(`/${page}`),
})),
]);
</script>

View File

@@ -1,13 +0,0 @@
<template>
<div v-if="tools" class="flex flex-row gap-1 flex-wrap">
<UBadge v-for="tool in tools" :key="tool" size="md" variant="solid">
{{ tool }}
</UBadge>
</div>
</template>
<script setup lang="ts">
const { tools } = defineProps<{
tools: string[];
}>();
</script>

View File

@@ -1,14 +0,0 @@
<template>
<UPageCard class="bg-background-100 my-10">
<p class="text-xl">
<slot />
</p>
<UiBadgeList :tools="tools" />
</UPageCard>
</template>
<script setup lang="ts">
const { tools } = defineProps<{
tools: string[];
}>();
</script>

View File

@@ -1,35 +0,0 @@
<template>
<UPageCard class="bg-background-100 my-10">
<p class="text-xl">
{{ $t('pages.vocal-synthesis.projects') }}
</p>
<div class="flex flex-col max-w gap-10">
<div v-for="project in data?.projects" :key="project.title" class="flex flex-row max-w gap-5">
<div>
<div
class="bg-primary text-text-50 dark:bg-primary p-1 rounded-md min-w-13 w-13 h-13 min-h-13 flex justify-center my-2"
>
<UIcon :name="project.icon" class="size-11" />
</div>
</div>
<div class="flex flex-col">
<div class="flex flex-row gap-2 items-baseline">
<ULink :to="project.link" class="text-2xl">
{{ project.title }}
</ULink>
<UIcon v-if="external(project.link)" name="mdi:link" class="size-5" />
</div>
<div>
{{ project.description }}
</div>
</div>
</div>
</div>
</UPageCard>
</template>
<script setup lang="ts">
// Inject data provided by the page to avoid hydration issues with MDC components
const data = inject('pageData');
const external = (url: string) => url.startsWith('http');
</script>

View File

@@ -1,8 +0,0 @@
<template>
<UiBadgeListCard v-if="data" :tools="data.tools">{{ $t('pages.vocal-synthesis.tools') }}</UiBadgeListCard>
</template>
<script setup lang="ts">
// Inject data provided by the page to avoid hydration issues with MDC components
const data = inject('pageData');
</script>

View File

@@ -1,26 +0,0 @@
<template>
<UDropdownMenu :key="locale" :items="availableLocales" :content="{ align: 'start' }">
<UButton color="neutral" variant="outline" icon="material-symbols:globe" :aria-label="$t('menu.language')" />
</UDropdownMenu>
</template>
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui';
const { locale, locales, setLocale } = useI18n();
const availableLocales = computed(() => {
return locales.value.map(
(optionLocale) =>
({
label: optionLocale.name,
code: optionLocale.code,
type: 'checkbox' as const,
checked: optionLocale.code === locale.value,
onUpdateChecked: () => switchLocale(optionLocale.code),
}) as DropdownMenuItem,
);
});
const switchLocale = (newLocale: string) => {
setLocale(newLocale);
};
</script>

View File

@@ -1,27 +0,0 @@
<template>
<UDropdownMenu :key="colorMode.preference" :items="themes" :content="{ align: 'start' }">
<UButton color="neutral" variant="outline" :icon="icons[currentColor]" :aria-label="$t('menu.theme')" />
</UDropdownMenu>
</template>
<script setup lang="ts">
type Theme = 'light' | 'dark' | 'system';
const icons: Dictionary<Theme, string> = {
light: 'material-symbols:light-mode',
dark: 'material-symbols:dark-mode',
system: 'material-symbols:computer-outline',
};
const colorMode = useColorMode();
const currentColor = computed<Theme>(() => colorMode.preference ?? 'system');
const themes = computed<DropdownValue[]>(() =>
['light', 'dark', 'system'].map((theme) => ({
code: theme,
label: $t(`theme.${theme}`),
icon: icons[theme],
type: 'checkbox' as const,
checked: currentColor.value === theme,
onUpdateChecked: () => switchColor(theme),
})),
);
const switchColor = (theme: Theme) => (colorMode.preference = theme);
</script>

View File

@@ -1,32 +0,0 @@
import type { FetchOptions } from 'ofetch';
export const useApi = () => {
const config = useRuntimeConfig();
const apiFetch = $fetch.create({
baseURL: config.public.apiBase,
});
const get = <T>(url: string, options?: FetchOptions) => apiFetch<T>(url, { method: 'GET', ...options });
const post = <ResultT, PayloadT = Record<string, string | number | boolean>>(
url: string,
body?: PayloadT,
options?: FetchOptions,
) => apiFetch<ResultT>(url, { method: 'POST', body, ...options });
const put = <ResultT, PayloadT = Record<string, string | number | boolean>>(
url: string,
body?: PayloadT,
options?: FetchOptions,
) => apiFetch<ResultT>(url, { method: 'PUT', body, ...options });
const patch = <ResultT, PayloadT = Record<string, string | number | boolean>>(
url: string,
body?: PayloadT,
options?: FetchOptions,
) => apiFetch<ResultT>(url, { method: 'PATCH', body, ...options });
const del = <T>(url: string, options?: FetchOptions) => apiFetch<T>(url, { method: 'DELETE', ...options });
return { get, post, put, patch, del };
};

View File

@@ -1,8 +0,0 @@
export const useBackend = () => {
const api = useApi();
const getMeta = () => api.get<MetaResponse>('/meta');
const postContact = (contact: ContactRequest) => api.post<ContactRequest, ContactResponse>('/contact', contact);
return { getMeta, postContact };
};

View File

@@ -1,65 +0,0 @@
import { withLeadingSlash } from 'ufo';
import type { Collections } from '@nuxt/content';
export const useDataJson = (prefix: string) => {
const route = useRoute();
const { locale } = useI18n();
const slug = computed(() => {
// Use route.params.slug for dynamic routes, or route.path for static routes
const slugValue = route.params.slug || route.path;
return withLeadingSlash(String(slugValue));
});
const key = computed(() => prefix + '-' + slug.value);
const getData = async <T>(
collectionPrefix: string,
options: {
useFilter?: boolean;
fallbackToEnglish?: boolean;
extractMeta?: boolean;
} = {},
) => {
const { useFilter = false, fallbackToEnglish = false, extractMeta = false } = options;
const { data } = await useAsyncData(
key.value,
async () => {
const collection = (collectionPrefix + locale.value) as keyof Collections;
let content;
if (useFilter) {
// For data collections, use .all() and filter
const allData = await queryCollection(collection).all();
content = allData.filter((source) => source.meta.path == slug.value)[0];
} else {
// For page collections, use .path().first()
content = await queryCollection(collection).path(slug.value).first();
if (!content && fallbackToEnglish && locale.value !== 'en') {
content = await queryCollection('content_en').path(slug.value).first();
}
}
return extractMeta ? content?.meta : content;
},
{
watch: [locale], // Automatically refresh when locale changes
},
);
return data as Ref<T | null>;
};
const getJsonData = async (collectionPrefix: string = 'content_data_') => {
return getData(collectionPrefix, { useFilter: true, extractMeta: true });
};
const getPageContent = async (collectionPrefix: string = 'content_', fallbackToEnglish: boolean = true) => {
return getData(collectionPrefix, { fallbackToEnglish });
};
const getCachedData = () => {
const { data } = useNuxtData(key.value);
return data;
};
return { getJsonData, getPageContent, getCachedData };
};

View File

@@ -1,27 +0,0 @@
export interface MetaImageOptions {
url: string;
alt: string;
}
export interface MetaOptions {
title: string;
description: string;
image?: MetaImageOptions;
}
export const useMeta = (options: MetaOptions) => {
const titleSuffix = ' Lucien Cartier-Tilet';
useSeoMeta({
title: () => options.title + titleSuffix,
ogTitle: () => options.title + titleSuffix,
twitterTitle: () => options.title + titleSuffix,
description: () => options.description,
ogDescription: () => options.description,
twitterDescription: () => options.description,
twitterCard: options.image ? 'summary_large_image' : 'summary',
ogImage: () => options.image?.url,
ogImageAlt: () => options.image?.alt,
twitterImage: () => options.image?.url,
twitterImageAlt: () => options.image?.alt,
});
};

View File

@@ -1,5 +0,0 @@
<template>
<div class="text-center prose prose-lg mx-auto max-w-prose">
<slot />
</div>
</template>

View File

@@ -1,5 +0,0 @@
<template>
<div class="min-h-screen mx-auto px-4 py-8 max-w-6xl">
<slot />
</div>
</template>

View File

@@ -1,22 +0,0 @@
<template>
<NuxtLayout v-if="page" :name="page.meta?.layout ?? 'default'">
<ContentRenderer :value="page" />
</NuxtLayout>
<div v-else>
<h1>Page not found</h1>
<p>This page doesn&apos;t exist in {{ locale }} language.</p>
</div>
</template>
<script setup lang="ts">
const { getPageContent } = useDataJson('page');
const page = await getPageContent();
// Pre-fetch JSON data for MDC components to avoid hydration issues
const { getJsonData } = useDataJson('page-data');
const pageData = await getJsonData();
// Provide data to child MDC components
provide('pageData', pageData);
useMeta({ title: page.value?.title, description: page.value?.description });
</script>

View File

@@ -1,47 +0,0 @@
<template>
<NuxtLayout name="default">
<h1 class="text-4xl text-highlighted font-bold mb-8">
{{ $t('pages.resume.name') }}
</h1>
<UPageCard class="bg-background-100 my-10">
<p>
{{ $t('pages.resume.experience') }}
</p>
<UTimeline v-model="valueExp" reverse :items="resumeContent?.experience" class="w-full">
<template #description="{ item }">
<div class="flex flex-col gap-2">
<p>
{{ item.description }}
</p>
<UiBadgeList :tools="item.tools" />
</div>
</template>
</UTimeline>
</UPageCard>
<UPageCard class="bg-background-100 my-10">
<p>
{{ $t('pages.resume.education') }}
</p>
<UTimeline v-model="valueEd" reverse :items="resumeContent?.education" class="w-full" />
</UPageCard>
<UiBadgeListCard :tools="resumeContent?.otherTools">{{ $t('pages.resume.tools') }}</UiBadgeListCard>
<UiBadgeListCard :tools="resumeContent?.devops">{{ $t('pages.resume.devops') }}</UiBadgeListCard>
<UiBadgeListCard :tools="resumeContent?.os">{{ $t('pages.resume.os') }}</UiBadgeListCard>
<UiBadgeListCard :tools="resumeContent?.programmingLanguages">{{
$t('pages.resume.programmingLanguages')
}}</UiBadgeListCard>
<UiBadgeListCard :tools="resumeContent?.frameworks">{{ $t('pages.resume.frameworks') }}</UiBadgeListCard>
</NuxtLayout>
</template>
<script setup lang="ts">
useMeta({
title: $t('pages.resume.name'),
description: $t('pages.resume.description'),
});
const { getJsonData } = useDataJson('resume');
const resumeContent = await getJsonData();
const arrLength = (array?: T[]) => (array ? array.length - 1 : 0);
const valueExp = computed(() => arrLength(resumeContent.value?.experience));
const valueEd = computed(() => arrLength(resumeContent.value?.education));
</script>

View File

@@ -1,11 +0,0 @@
export interface ContactRequest {
name: string;
email: string;
message: string;
website?: string | null;
}
export interface ContactResponse {
success: boolean;
message: string;
}

View File

@@ -1,4 +0,0 @@
export interface ApiError {
message: string;
success: boolean;
}

View File

@@ -1,4 +0,0 @@
export interface MetaResponse {
version: string;
name: string;
}

View File

@@ -1,3 +0,0 @@
export interface Dictionary<K, T> {
[key: K]: T;
}

View File

@@ -1,13 +0,0 @@
export interface ResumeExperience extends TimelineItem {
tools: string[];
}
export interface ResumeContent {
experience: ResumeExperience[];
education: TimelineItem[];
otherTools: string[];
devops: string[];
os: string[];
programmingLanguages: string[];
frameworks: string[];
}

View File

@@ -1,42 +0,0 @@
import { defineCollection, defineContentConfig } from '@nuxt/content';
import { z } from 'zod';
const commonSchema = z.object({
title: z.string(),
description: z.string()
});
export default defineContentConfig({
collections: {
content_en: defineCollection({
type: 'page',
source: {
include: 'en/**/*.md',
prefix: '',
},
schema: commonSchema,
}),
content_fr: defineCollection({
type: 'page',
source: {
include: 'fr/**/*.md',
prefix: '',
},
schema: commonSchema,
}),
content_data_en: defineCollection({
type: 'data',
source: {
include: 'en/**/*.json',
prefix: ''
},
}),
content_data_fr: defineCollection({
type: 'data',
source: {
include: 'fr/**/*.json',
prefix: ''
},
}),
},
});

View File

@@ -1,12 +0,0 @@
---
layout: centered
title: Home
description: Personal Website
---
# Welcome
Web Developer • Worldbuilder • Conlanger
Hi, I'm Lucien Cartier-Tilet. I work as a web developer and consultant. Outside of work, I spend time on worldbuilding
and constructed languages.

View File

@@ -1,66 +0,0 @@
{
"experience": [
{
"date": "Since Septembre 2023",
"title": "Consultant Aubay",
"description": "Web development consultant working on enterprise applications. Continued focus on Angular front-end development and Java Spring Boot back-end services with PostgreSQL databases.",
"tools": [
"Angular",
"TypeScript",
"Java Spring Boot",
"Java Spring Batch",
"PostgreSQL",
"VS Code",
"Eclipse",
"IntelliJ Idea",
"Git"
],
"icon": "mdi:laptop"
},
{
"date": "February 2023 August 2023",
"title": "Intern Aubay",
"description": "Web application development internship focused on full-stack development. Worked on projects using Angular for front-end and Java Spring Boot for back-end, with PostgreSQL databases.",
"tools": ["Angular", "TypeScript", "Java Spring Boot", "PostgreSQL", "VS Code", "Eclipse", "Git"],
"icon": "mdi:book"
},
{
"date": "October 2014 July 2018",
"title": "CTO Voxwave",
"description": "Co-founded a startup specialized in creating French virtual singers using vocal synthesis. Developed singing synthesis vocal libraries, conducted linguistic research, provided user support, and trained recruits in vocal library development. Led technical development of ALYS, the first professional French singing voice library.",
"tools": ["Alter/Ego", "UTAU", "FL Studio", "iZotope RX", "T-RackS CS"],
"icon": "mdi:waveform"
}
],
"education": [
{
"date": "September 2022 September 2023",
"title": "Master's Degree in Hypermedia Technologies University of Paris 8",
"description": "Obtained Master's degree in THYP (Hypermedia Technologies) on 11 September 2023. Repeated the year for health reasons without any lasting effects.",
"icon": "mdi:network"
},
{
"date": "September 2020 September 2021",
"title": "Master's Degree in Computer Science University of Paris 8",
"description": "First year of my Masters degree.",
"icon": "mdi:code-tags"
},
{
"date": "September 2016 July 2019",
"title": "Bachelor's Degree in Computer Science University of Paris 8",
"description": "Bachelor's degree in Computer Science obtained in July 2019",
"icon": "mdi:school-outline"
},
{
"date": "Septembre 2013 Décembre 2014",
"title": "English Literature Université Lyon 2",
"description": "One and a half years of literary English studies in an LLCE English degree. Studies interrupted following the creation of VoxWave.",
"icon": "mdi:book-open-page-variant"
}
],
"otherTools": ["Emacs", "Vim", "jj", "PostgreSQL", "SQLite"],
"devops": ["GitHub", "Gitlab", "Gitea", "GitHub Actions", "Drone.io", "Docker", "Podman"],
"os": ["NixOS", "Debian", "Arch Linux", "Void Linux", "Alpine Linux", "Windows"],
"programmingLanguages": ["TypeScript", "Rust", "C", "EmacsLisp", "Bash/Zsh", "C++", "Python", "CommonLisp"],
"frameworks": ["Angular", "Vue", "Nuxt", "Spring Boot", "Poem (Rust)"]
}

View File

@@ -1,35 +0,0 @@
{
"projects": [
{
"title": "BSUP01 KEINE Tashi series",
"icon": "mdi:microphone",
"description": "Released starting October 2012. My second vocal library, recorded with better equipment than my first attempt. The series included several Japanese vocal libraries, with the Extend Power version being my best work in this series.",
"link": "/keine-tashi"
},
{
"title": "First Tibetan vocal libraries",
"icon": "mdi:earth",
"description": "BSUP01 KEINE Tashi and BSUP02 Drölma were the first Tibetan vocal libraries for singing synthesis worldwide.",
"link": "/keine-tashi"
},
{
"title": "ALYS prototypes for UTAU",
"icon": "mdi:flask",
"description": "Created ALYS 001 JPN, ALYS 001 FRA, and ALYS 002 FRA as test versions while working at VoxWave. Known as ALYS4UTAU.",
"link": "https://alys.phundrak.com/en/"
},
{
"title": "ALYS for Alter/Ego",
"icon": "mdi:package",
"description": "The first commercial vocal library for Alter/Ego, and the first professional French singing vocal library. Development took well over a year, with eight to nine additional months for the first major update. Now available free of charge.",
"link": "https://alys.phundrak.com/en/"
},
{
"title": "LEORA",
"description": "A French singing vocal library developed at VoxWave alongside ALYS.",
"icon": "mdi:music",
"link": "https://alys.phundrak.com/en/faq#are-there-any-plans-for-leora"
}
],
"tools": ["Alter/Ego", "UTAU", "VOCALOID", "ChipSpeech", "FL Studio", "Audacity", "iZotope RX", "T-RackS CS", "C++"]
}

View File

@@ -1,17 +0,0 @@
---
title: Vocal Synthesis
description:
Vocal synthesis projects from 2011-2018, including ALYS, the first professional French singing voice library for
Alter/Ego.
---
# Vocal Synthesis
I worked in singing vocal synthesis from 2011 to 2018. I created vocal libraries for UTAU and Alter/Ego, including the
first professional French singing voice library.
:::VocalSynthProjects
:::
:::VocalSynthTools
:::

View File

@@ -1,12 +0,0 @@
---
layout: centered
title: Accueil
description: Site web personnel
---
# Bienvenue
Développeur web • Créateur dunivers • Idéolinguiste
Bonjour, je m'appelle Lucien Cartier-Tilet. Je travaille comme développeur web et consultant. En dehors du travail, je
consacre mon temps à la création d'univers et de langues construites.

View File

@@ -1,66 +0,0 @@
{
"experience": [
{
"date": "Depuis septembre 2023",
"title": "Consultant Aubay",
"description": "Consultant en développement web travaillant sur des applications d'entreprise. Je continue à me concentrer sur le développement front-end Angular et les services back-end Java Spring Boot avec des bases de données PostgreSQL.",
"tools": [
"Angular",
"TypeScript",
"Java Spring Boot",
"Java Spring Batch",
"PostgreSQL",
"VS Code",
"Eclipse",
"IntelliJ Idea",
"Git"
],
"icon": "mdi:laptop"
},
{
"date": "Février 2023 Août 2023",
"title": "Stagiaire Aubay",
"description": "Stage en développement d'applications web axé sur le développement full-stack. J'ai travaillé sur des projets utilisant Angular pour le front-end et Java Spring Boot pour le back-end, avec des bases de données PostgreSQL.",
"tools": ["Angular", "TypeScript", "Java Spring Boot", "PostgreSQL", "VS Code", "Eclipse", "Git"],
"icon": "mdi:book"
},
{
"date": "Octobre 2014 Juillet 2018",
"title": "Directeur technique Voxwave",
"description": "Co-fondateur d'une start-up spécialisée dans la création de chanteurs virtuels français à l'aide de la synthèse vocale. Développement de banques vocales de synthèse chantée, recherche linguistique, assistance aux utilisateurs et formation des recrues au développement de banques vocales. Direction du développement technique d'ALYS, la première banques vocale professionnelle de chant en français.",
"tools": ["Alter/Ego", "UTAU", "FL Studio", "iZotope RX", "T-RackS CS"],
"icon": "mdi:waveform"
}
],
"education": [
{
"date": "Septembre 2022 Septembre 2023",
"title": "Master 2 Technologies de lHypermédia Université Paris 8",
"description": "Obtention du diplôme Master 2 THYP le 11 septembre 2023. Redoublement pour causes de santé sans séquelles.",
"icon": "mdi:network"
},
{
"date": "Septembre 2020 Septembre 2021",
"title": "Master 1 Informatique Université Paris 8",
"description": "",
"icon": "mdi:code-tags"
},
{
"date": "Septembre 2016 Juillet 2019",
"title": "Licence Informatique Université Paris 8",
"description": "Licence dInformatique obtenue en Juillet 2019",
"icon": "mdi:school-outline"
},
{
"date": "Septembre 2013 Décembre 2014",
"title": "Anglais LLCE Université Lyon 2",
"description": "Un an et demi détudes danglais littéraire en licence danglais LLCE. Études interrompues suite à la création de VoxWave.",
"icon": "mdi:book-open-page-variant"
}
],
"otherTools": ["Emacs", "Vim", "jj", "PostgreSQL", "SQLite"],
"devops": ["GitHub", "Gitlab", "Gitea", "GitHub Actions", "Drone.io", "Docker", "Podman"],
"os": ["NixOS", "Debian", "Arch Linux", "Void Linux", "Alpine Linux", "Windows"],
"programmingLanguages": ["TypeScript", "Rust", "C", "EmacsLisp", "Bash/Zsh", "C++", "Python", "CommonLisp"],
"frameworks": ["Angular", "Vue", "Nuxt", "Spring Boot", "Poem (Rust)"]
}

View File

@@ -1,35 +0,0 @@
{
"projects": [
{
"title": "BSUP01 KEINE Tashi",
"icon": "mdi:microphone",
"description": "Sortie en octobre 2012. Ma deuxième bibliothèque vocale, enregistrée avec un équipement de meilleure qualité que ma première tentative. La série comprenait plusieurs bibliothèques vocales japonaises, la version Extend Power étant ma meilleure réalisation dans cette série.",
"link": "/keine-tashi"
},
{
"title": "Premières bibliothèques vocales tibétaines",
"icon": "mdi:earth",
"description": "BSUP01 KEINE Tashi et BSUP02 Drölma ont été les premières bibliothèques vocales tibétaines au monde dédiées à la synthèse vocale chantée.",
"link": "/keine-tashi"
},
{
"title": "Prototypes ALYS pour UTAU",
"icon": "mdi:flask",
"description": "Création des versions test ALYS 001 JPN, ALYS 001 FRA et ALYS 002 FRA chez VoxWave. Connue sous le nom d'ALYS4UTAU.",
"link": "https://alys.phundrak.com"
},
{
"title": "ALYS pour Alter/Ego",
"icon": "mdi:package",
"description": "La première bibliothèque vocale commerciale pour Alter/Ego, et la première bibliothèque vocale professionnelle en français. Son développement a pris plus d'un an, avec huit à neuf mois supplémentaires pour la première mise à jour majeure. Elle est désormais disponible gratuitement.",
"link": "https://alys.phundrak.com"
},
{
"title": "LEORA",
"description": "Une bibliothèque vocale française développée chez VoxWave en collaboration avec ALYS.",
"icon": "mdi:music",
"link": "https://alys.phundrak.com/faq#y-a-t-il-quelque-chose-de-prevu-pour-leora"
}
],
"tools": ["Alter/Ego", "UTAU", "VOCALOID", "ChipSpeech", "FL Studio", "Audacity", "iZotope RX", "T-RackS CS", "C++"]
}

View File

@@ -1,15 +0,0 @@
---
title: Synthèse vocale
description:Projets de synthèse vocale de 2011 à 2018, dont ALYS, la première bibliothèque professionnelle de voix chantées en français pour Alter/Ego.
---
# Synthèse vocale
J'ai travaillé dans le domaine de la synthèse vocale chantée de 2011 à 2018. J'ai créé des bibliothèques vocales pour
UTAU et Alter/Ego, notamment la première bibliothèque professionnelle de voix chantées en français.
:::VocalSynthProjects
:::
:::VocalSynthTools
:::

View File

@@ -1,6 +0,0 @@
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(
// Your custom configs here
)

View File

@@ -1,15 +0,0 @@
export default defineI18nConfig(() => ({
legacy: false,
locale: 'en',
messages: {
en: {
welcome: 'Welcome',
},
fr: {
welcome: 'Bienvenue',
},
lfn: {
welcome: 'Bonveni',
},
},
}));

View File

@@ -1,55 +0,0 @@
{
"website": {
"name": "Lucien Cartier-Tilet",
"langSwitch": "Language"
},
"menu": {
"name": "Menu",
"language": "Language Selector",
"theme": "Theme Selector"
},
"theme": {
"name": "theme",
"dark": "Dark",
"light": "Light",
"system": "Auto"
},
"pages": {
"home": {
"name": "Home"
},
"resume": {
"name": "Resume",
"description": "",
"experience": "Experience",
"education": "Education",
"tools": "Tools",
"devops": "Devops Tools",
"os": "Operating Systems",
"programmingLanguages": "Programming Languages",
"frameworks": "Frameworks"
},
"vocal-synthesis": {
"name": "Vocal Synthesis",
"projects": "Key Projects",
"tools": "Tools"
},
"languages": {
"name": "Languages & Worldbuilding"
},
"contact": {
"name": "Contact"
}
},
"footer": {
"links": {
"source": "Websites source code",
"nuxt": "Frontend made with Nuxt",
"rust": "Backend made with Rust"
},
"versions": {
"frontend": "Frontend Version",
"backend": "Backend Version"
}
}
}

View File

@@ -1,55 +0,0 @@
{
"website": {
"name": "Lucien Cartier-Tilet",
"langSwitch": "Language"
},
"menu": {
"name": "Menu",
"language": "Choix de la langue",
"theme": "Thème du site web"
},
"theme": {
"name": "Thème",
"dark": "Sombre",
"light": "Clair",
"system": "Auto"
},
"pages": {
"home": {
"name": "Accueil"
},
"resume": {
"name": "CV",
"description": "",
"experience": "Expérience",
"education": "Éducation",
"tools": "Outils",
"devops": "Outils Devops",
"os": "Systèmes dexploitation",
"programmingLanguages": "Langages de programmation",
"frameworks": "Frameworks"
},
"vocal-synthesis": {
"name": "Synthèse Vocale",
"projects": "Projets principaux",
"tools": "Outils"
},
"languages": {
"name": "Langues et Univers Fictifs"
},
"contact": {
"name": "Contact"
}
},
"footer": {
"links": {
"source": "Code source du site web",
"nuxt": "Frontend fait avec Nuxt",
"rust": "Backend fait avec Rust"
},
"versions": {
"frontend": "Frontend Version",
"backend": "Backend Version"
}
}
}

View File

@@ -1,80 +0,0 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: {
enabled: true,
vueDevTools: true,
telemetry: false,
},
modules: [
'@nuxt/eslint',
'@nuxt/image',
'@nuxt/test-utils',
'@nuxt/ui',
'@nuxt/content',
'@nuxtjs/i18n',
'@nuxtjs/turnstile',
'@nuxtjs/device',
'@nuxt/icon',
'@nuxt/fonts',
'@nuxtjs/color-mode',
'@nuxtjs/tailwindcss',
],
css: ['~/assets/css/main.css'],
content: {
database: {
type: 'sqlite',
filename: '.data/content/contents.sqlite',
},
},
i18n: {
locales: [
{ code: 'en', name: 'English', language: 'en-UK', file: 'en.json' },
{ code: 'fr', name: 'Français', language: 'fr-FR', file: 'fr.json' },
// { code: 'lfn', name: 'Elefen', language: 'lfn', file: 'lfn.json' },
// { code: 'ei', name: 'Eittlandic', language: 'ei-ST', file: 'ei.json' },
],
strategy: 'no_prefix',
defaultLocale: 'en',
},
fonts: {
provider: 'google',
processCSSVariables: true,
defaults: {
weights: [400, 700],
styles: ['normal', 'italic'],
},
families: [
{ name: 'Noto Sans', provider: 'google' },
{ name: 'Wittgenstein', provider: 'google' }
]
},
icon: {
serverBundle: {
collections: ['material-symbols', 'mdi']
},
clientBundle: {
scan: true,
}
},
postcss: {
plugins: {
'@tailwindcss/postcss': {},
'autoprefixer': {}
}
},
turnstile: {
siteKey: '', // Overridden by NUXT_PUBLIC_TURNSTILE_SITE_KEY
addValidateEndpoint: true
},
runtimeConfig: {
turnstile: {
secretKey: '', // Overriden by NUXT_TURNSTILE_SECRET_KEY
},
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE || 'http://localhost:3100/api',
}
},
});

View File

@@ -1,57 +0,0 @@
{
"name": "frontend",
"type": "module",
"private": true,
"version": "0.1.0",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"cleanup": "nuxt cleanup",
"lint": "eslint .",
"format": "prettier --write app/ i18n/ content/",
"format-check": "prettier --check app/ i18n/ content/"
},
"dependencies": {
"@nuxt/content": "3.8.0",
"@nuxt/eslint": "1.10.0",
"@nuxt/fonts": "0.12.1",
"@nuxt/icon": "2.1.0",
"@nuxt/image": "1.11.0",
"@nuxt/scripts": "^0.12.2",
"@nuxt/test-utils": "3.20.1",
"@nuxt/ui": "4.1.0",
"@nuxtjs/color-mode": "3.5.2",
"@nuxtjs/device": "3.2.4",
"@nuxtjs/tailwindcss": "7.0.0-beta.0",
"@nuxtjs/turnstile": "1.1.1",
"better-sqlite3": "^12.4.1",
"eslint": "^9.39.1",
"nitropack": "^2.12.9",
"nuxi": "^3.30.0",
"nuxt": "^4.2.0",
"typescript": "^5.9.3",
"vite": "^7.1.12",
"vue": "^3.5.22",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@iconify-json/material-symbols": "^1.2.44",
"@iconify-json/material-symbols-light": "^1.2.44",
"@iconify-json/mdi": "^1.2.3",
"@nuxtjs/i18n": "^10.2.0",
"@tailwindcss/postcss": "^4.1.17",
"autoprefixer": "^10.4.22",
"less": "^4.4.2",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"zod": "^4.1.12"
},
"pnpm": {
"overrides": {
"sharp": "0.33.4"
}
}
}

12952
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
onlyBuiltDependencies:
- '@parcel/watcher'
- better-sqlite3
- esbuild
- sharp
- unrs-resolver
- vue-demi

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,2 +0,0 @@
User-Agent: *
Disallow:

View File

@@ -1,54 +0,0 @@
{
inputs,
pkgs,
self,
...
}:
inputs.devenv.lib.mkShell {
inherit inputs pkgs;
modules = [
{
devenv.root = let
devenvRootFileContent = builtins.readFile "${self}/.devenv-root";
in
pkgs.lib.mkIf (devenvRootFileContent != "") devenvRootFileContent;
}
{
env.PNPM_HOME = "${self}/.pnpm-store";
packages = with pkgs; [
# LSP
marksman
# nodePackages."@tailwindcss/language-server"
# nodePackages."@vue/language-server"
# vscode-langservers-extracted
rustywind
nodePackages.prettier
nodePackages.eslint
# Node
nodejs_24
nodePackages.pnpm
# typescript
# nodePackages.typescript-language-server
];
enterShell = ''
echo "🚀 Nuxt.js development environment loaded!"
echo "📦 Node.js version: $(node --version)"
echo "📦 pnpm version: $(pnpm --version)"
echo ""
echo "Available LSP servers:"
echo " - typescript-language-server (TypeScript)"
echo " - vue-language-server (Vue/Volar)"
echo " - tailwindcss-language-server (Tailwind CSS)"
echo " - vscode-langservers-extracted (HTML, CSS, JSON, ESLint)"
echo ""
echo "Run 'pnpm install' to install dependencies"
echo "Run 'pnpm dev' to start the development server"
'';
}
];
}

View File

@@ -1,18 +0,0 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"files": [],
"references": [
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
}
]
}

View File

@@ -1,15 +1,8 @@
{
rust-overlay,
inputs,
system,
pkgs,
rustPlatform,
...
}: let
rust = import ./rust-version.nix { inherit rust-overlay inputs system; };
pkgs = rust.pkgs;
rustPlatform = pkgs.makeRustPlatform {
cargo = rust.version;
rustc = rust.version;
};
cargoToml = builtins.fromTOML (builtins.readFile ../Cargo.toml);
name = cargoToml.package.name;
version = cargoToml.package.version;

View File

@@ -1,4 +1,9 @@
{rust-overlay, inputs, system, ...}: let
{
rust-overlay,
inputs,
system,
...
}: let
overlays = [(import rust-overlay)];
in rec {
pkgs = import inputs.nixpkgs {inherit system overlays;};

66
nix/shell.nix Normal file
View File

@@ -0,0 +1,66 @@
{
inputs,
pkgs,
self,
rustVersion,
...
}:
inputs.devenv.lib.mkShell {
inherit inputs pkgs;
modules = [
{
packages = with pkgs; [
(rustVersion.override {
extensions = [
"clippy"
"rust-src"
"rust-analyzer"
"rustfmt"
];
})
bacon
cargo-deny
cargo-shuttle
cargo-tarpaulin
cargo-watch
flyctl
just
marksman
tombi # TOML lsp server
];
services.mailpit = {
enable = true;
# HTTP interface for viewing emails
uiListenAddress = "127.0.0.1:8025";
# SMTP server for receiving emails
smtpListenAddress = "127.0.0.1:1025";
};
processes.run.exec = "cargo watch -x run";
enterShell = ''
echo "🦀 Rust backend development environment loaded!"
echo "📦 Rust version: $(rustc --version)"
echo "📦 Cargo version: $(cargo --version)"
echo ""
echo "Available tools:"
echo " - rust-analyzer (LSP)"
echo " - clippy (linter)"
echo " - rustfmt (formatter)"
echo " - bacon (continuous testing/linting)"
echo " - cargo-deny (dependency checker)"
echo " - cargo-tarpaulin (code coverage)"
echo ""
echo "📧 Mailpit service:"
echo " - SMTP server: 127.0.0.1:1025"
echo " - Web UI: http://127.0.0.1:8025"
echo ""
echo "🚀 Quick start:"
echo " Run 'devenv up' to launch:"
echo " - Mailpit service (email testing)"
echo " - Backend with 'cargo watch -x run' (auto-reload)"
'';
}
];
}