diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e03fca..31ba0b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,24 @@ migrates `[Unreleased]` into that file and empties this one — see diagonal stripes and the subtitle retypes like the titles. Still under `prefers-reduced-motion`. +- **A journey page, `/experience`** + ([#78](https://github.com/kbrdn1/kbrdn.dev/issues/78)). Laid out like an + editorial spread (after isrod.eu): an intro, a strip of every role and + degree, then one spread each, the story on the left and the stack and + missions on the right, with "show more". Then the open source work, every + item linked to its pull request or issue: the projects I maintain (gwm, + vellum, LazyCurl, herdr-plugin-gwm, claude-crosstalk, kbrdn-docs), the + contributions (winget, aqua-registry and awesome-ratatui merged, nixpkgs in + review) and the bugs reported upstream. On the home page, the Parcours + accordions give way to a one-line-per-role teaser linking to it. + +- **An apps page, `/apps`** + ([#79](https://github.com/kbrdn1/kbrdn.dev/issues/79)). gwm (available), + vellum (in development) and LazyCurl (archived), each with its features and + licence, links to its site, repository and releases, and for gwm the install + commands of its README. The latest release is read from GitHub instead of + written down. + ### Changed - **A new logo** diff --git a/app/assets/css/components.css b/app/assets/css/components.css index c7721ab..0fab0f4 100644 --- a/app/assets/css/components.css +++ b/app/assets/css/components.css @@ -61,19 +61,6 @@ background-color: var(--color-primary-500); } -/* Accordion +/- icon animation */ -.accordion-icon-bar { - transition: transform 300ms ease !important; -} - -.accordion-icon-bar.is-collapsed { - transform: rotate(90deg); -} - -.accordion-icon-bar.is-expanded { - transform: rotate(0deg); -} - /* Disabled button with subtle diagonal striped background */ .btn-disabled-striped { background-image: repeating-linear-gradient( diff --git a/app/assets/css/transitions.css b/app/assets/css/transitions.css index 24ddeed..5103263 100644 --- a/app/assets/css/transitions.css +++ b/app/assets/css/transitions.css @@ -2,25 +2,6 @@ Animations & Transitions ═══════════════════════════════════════════════════ */ -/* Accordion animation */ -.accordion-wrapper { - display: grid; - grid-template-rows: 0fr; - opacity: 0; - transition: - grid-template-rows 400ms cubic-bezier(0.4, 0, 0.2, 1), - opacity 300ms ease !important; -} - -.accordion-wrapper.is-open { - grid-template-rows: 1fr; - opacity: 1; -} - -.accordion-wrapper > .accordion-inner { - overflow: hidden; -} - /* Nav entrance animation */ @keyframes nav-fade-in { from { diff --git a/app/components/experience/Spread.vue b/app/components/experience/Spread.vue new file mode 100644 index 0000000..5e28b93 --- /dev/null +++ b/app/components/experience/Spread.vue @@ -0,0 +1,99 @@ + + + diff --git a/app/components/home/Experiences.vue b/app/components/home/Experiences.vue deleted file mode 100644 index 34fd9da..0000000 --- a/app/components/home/Experiences.vue +++ /dev/null @@ -1,227 +0,0 @@ - - - diff --git a/app/components/home/Parcours.vue b/app/components/home/Parcours.vue new file mode 100644 index 0000000..e7927ba --- /dev/null +++ b/app/components/home/Parcours.vue @@ -0,0 +1,55 @@ + + + diff --git a/app/components/home/Studies.vue b/app/components/home/Studies.vue deleted file mode 100644 index 933b3f7..0000000 --- a/app/components/home/Studies.vue +++ /dev/null @@ -1,268 +0,0 @@ - - - diff --git a/app/components/layout/Footer.vue b/app/components/layout/Footer.vue index 5d6f663..356b651 100644 --- a/app/components/layout/Footer.vue +++ b/app/components/layout/Footer.vue @@ -7,8 +7,8 @@ const portfolioLinks = computed(() => [ { href: '/#about', label: t('footer.nav.about') }, { href: '/#skills', label: t('footer.nav.skills') }, { href: '/#projects', label: t('footer.nav.projects') }, - { href: '/#experience', label: t('footer.nav.experience') }, - { href: '/#education', label: t('footer.nav.education') }, + { href: '/experience', label: t('footer.nav.experience') }, + { href: '/experience#education', label: t('footer.nav.education') }, ]) const blogLinks = computed(() => [{ href: '/blog', label: t('footer.nav.articles') }]) diff --git a/app/pages/apps.vue b/app/pages/apps.vue new file mode 100644 index 0000000..a9315c6 --- /dev/null +++ b/app/pages/apps.vue @@ -0,0 +1,256 @@ + + + diff --git a/app/pages/experience.vue b/app/pages/experience.vue new file mode 100644 index 0000000..9321447 --- /dev/null +++ b/app/pages/experience.vue @@ -0,0 +1,330 @@ + + + diff --git a/app/pages/index.vue b/app/pages/index.vue index 02fb5fc..a562293 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -196,26 +196,9 @@ const content = computed(() => {
-
+
- -
-

- {{ t('sections.experience') }} -

- -
- -
-

- {{ t('sections.education') }} -

- -
+
diff --git a/app/utils/career.ts b/app/utils/career.ts new file mode 100644 index 0000000..ce4d8b6 --- /dev/null +++ b/app/utils/career.ts @@ -0,0 +1,244 @@ +// The career and open source facts behind /experience and the home's Parcours teaser. +// Text lives in i18n (experiences.*, studies.*, experiencePage.openSource.*); this file +// only holds what does not translate: ids, dates, stacks, links. Open source entries are +// facts checked on GitHub: `merged` and `review` are pull requests, `report` an issue. + +export interface Experience { + id: string + key: string + company: string + logo: string + startDate: string + endDate: string | null + technologies: string[] + tasksCount: number +} + +export interface Study { + id: string + key: string + school: string + logo: string + level: string + startDate: string + endDate: string | null + technologies: string[] + skillsCount: number +} + +export const EXPERIENCES: Experience[] = [ + { + id: 'flippad', + key: 'flippad', + company: 'Jewely x Flippad', + logo: '/images/logos/jewely-x-flippad.png', + startDate: '2023-10', + endDate: '2026-09', + technologies: [ + 'Laravel', + 'Vue', + 'Nuxt', + 'AWS', + 'React', + 'Symfony', + 'Redis', + 'Stripe', + 'Hono.js', + 'Prisma', + ], + tasksCount: 5, + }, + { + id: 'virtual-immersion', + key: 'virtualImmersion', + company: 'Virtual Immersion', + logo: '/images/logos/virtual-immersion.png', + startDate: '2022-11', + endDate: '2023-09', + technologies: ['Symfony', 'Laravel', 'React', 'WordPress', 'MySQL', 'Tailwind CSS'], + tasksCount: 4, + }, +] + +export const STUDIES: Study[] = [ + { + id: 'mns-master', + key: 'mnsMaster', + school: 'Metz Numeric School', + logo: '/images/logos/mns.jpeg', + level: 'Master', + startDate: '2023-10', + endDate: '2025-10', + technologies: [ + 'C#/.NET', + 'TypeScript', + 'Node', + 'Git', + 'Docker', + 'Kubernetes', + 'Flutter', + 'Hono.js', + 'Prisma', + 'Drizzle', + 'Adonis.js', + ], + skillsCount: 5, + }, + { + id: 'mns-licence', + key: 'mnsLicence', + school: 'Metz Numeric School', + logo: '/images/logos/mns.jpeg', + level: 'Licence', + startDate: '2022-10', + endDate: '2023-09', + technologies: [ + 'Figma', + 'Java', + 'Symfony', + 'React', + 'MongoDB', + 'TypeScript', + 'Node', + 'Git', + 'Docker', + 'Vue', + 'Angular', + ], + skillsCount: 4, + }, + { + id: 'cci-bac2', + key: 'cciBac2', + school: 'CCI Longwy EESC', + logo: '/images/logos/cci.jpg', + level: 'BAC+2', + startDate: '2020-10', + endDate: '2022-06', + technologies: [ + 'HTML', + 'CSS', + 'PHP', + 'JavaScript', + 'SASS', + 'jQuery', + 'Symfony', + 'Adobe XD', + 'Photoshop', + 'Illustrator', + 'MySQL', + 'Bootstrap', + 'Git', + ], + skillsCount: 5, + }, +] + +/** Projects I maintain on GitHub; `status` is the site's label, not the repository's. */ +export const OPEN_SOURCE_PROJECTS = [ + { + key: 'gwm', + name: 'gwm', + language: 'Rust', + status: 'released', + repo: 'https://github.com/kbrdn1/gwm-cli', + site: 'https://gwm.kbrdn.dev', + // Install channels listed in the gwm-cli README + channels: ['crates.io', 'Homebrew', 'Scoop', 'aqua', 'Nix', '.deb', '.rpm', 'AUR'], + }, + { + key: 'vellum', + name: 'vellum', + language: 'Rust', + status: 'inDevelopment', + repo: 'https://github.com/kbrdn1/vellum', + }, + { + key: 'lazycurl', + name: 'LazyCurl', + language: 'Go', + status: 'archived', + repo: 'https://github.com/kbrdn1/LazyCurl', + }, + { + key: 'herdrPluginGwm', + name: 'herdr-plugin-gwm', + language: 'Shell', + status: 'released', + repo: 'https://github.com/kbrdn1/herdr-plugin-gwm', + }, + { + key: 'claudeCrosstalk', + name: 'claude-crosstalk', + language: 'TypeScript', + status: 'released', + repo: 'https://github.com/kbrdn1/claude-crosstalk', + }, + { + key: 'kbrdnDocs', + name: 'kbrdn-docs', + language: 'Astro', + status: 'released', + repo: 'https://github.com/kbrdn1/kbrdn-docs', + }, +] as const + +/** Pull requests and issues sent to projects I don't maintain, checked on GitHub. */ +export const UPSTREAM = [ + { + key: 'winget', + repo: 'microsoft/winget-pkgs', + kind: 'merged', + url: 'https://github.com/microsoft/winget-pkgs/pull/403295', + }, + { + key: 'aqua', + repo: 'aquaproj/aqua-registry', + kind: 'merged', + url: 'https://github.com/aquaproj/aqua-registry/pull/57117', + }, + { + key: 'awesomeRatatui', + repo: 'ratatui/awesome-ratatui', + kind: 'merged', + url: 'https://github.com/ratatui/awesome-ratatui/pull/403', + }, + { + key: 'nixpkgs', + repo: 'NixOS/nixpkgs', + kind: 'review', + url: 'https://github.com/NixOS/nixpkgs/pull/545222', + }, + { + key: 'snacks', + repo: 'folke/snacks.nvim', + kind: 'report', + url: 'https://github.com/folke/snacks.nvim/issues/2689', + }, + { + key: 'codexbar', + repo: 'steipete/CodexBar', + kind: 'report', + url: 'https://github.com/steipete/CodexBar/issues/420', + }, + { + key: 'terminalBrowser', + repo: 'zenbu-labs/terminal-browser', + kind: 'report', + url: 'https://github.com/zenbu-labs/terminal-browser/issues/85', + }, + { + key: 'elasticlens', + repo: 'pdphilip/elasticlens', + kind: 'report', + url: 'https://github.com/pdphilip/elasticlens/issues/16', + }, +] as const + +/** "oct. 2023" / "Oct 2023" */ +export function formatMonth(date: string, locale: string): string { + return new Date(date).toLocaleDateString(locale === 'fr' ? 'fr-FR' : 'en-US', { + month: 'short', + year: 'numeric', + }) +} diff --git a/i18n/locales/en.ts b/i18n/locales/en.ts index 0dcb168..72d91e5 100644 --- a/i18n/locales/en.ts +++ b/i18n/locales/en.ts @@ -50,8 +50,6 @@ export default { about: 'ABOUT', projects: 'PROJECTS', skills: 'STACK & SKILLS', - experience: 'EXPERIENCE', - education: 'EDUCATION', parcours: 'JOURNEY', activity: 'ACTIVITY', blog: 'BLOG', @@ -267,6 +265,145 @@ export default { }, // Blog Navigation + // /experience + experiencePage: { + seoTitle: 'Journey and open source · Kylian Bardini', + seoDescription: + 'The journey of Kylian Bardini, full stack developer: Jewely x Flippad, Virtual Immersion, Metz Numeric School, and the open source tools he publishes (gwm, vellum, LazyCurl).', + ogTitle: 'Journey', + ogSubtitle: 'Experience, education and open source', + eyebrow: 'JOURNEY', + title: 'Full stack developer, from code to production.', + intro: + 'Nearly four years in companies, starting as a work-study student: an ERP and back offices in Laravel and Symfony, front ends in Vue, Nuxt and React, deployed on AWS and Heroku. On the side, open source tools for the terminal, in Rust and Go.', + ctaOpenSource: 'See the open source', + ctaBlog: 'Read the blog', + timeline: 'Roles and education', + experienceLabel: 'EXPERIENCE', + educationLabel: 'EDUCATION', + stack: 'Stack', + missions: 'Missions', + skills: 'Learned', + more: 'Show more', + less: 'Show less', + summaries: { + flippad: + 'Three years at Jewely x Flippad, first as a work-study student, then full time: an ERP for luxury jewellers, a multi-brand jewellery configurator, the Assmat-Facile back office and the Fiches Pédagogiques rebuild, deployed on AWS and Heroku through GitHub Actions.', + virtualImmersion: + 'A work-study year at Virtual Immersion: a file management API in Symfony, mixed reality on WordPress and the map navigation of an animal park in React.', + mnsMaster: + 'Software engineering: advanced software architecture and microservices, testing at every level, continuous integration and delivery, and two years of project management for the degree.', + mnsLicence: + 'Full stack developer: UI and UX with Figma, web, mobile and desktop frameworks, software architecture principles, and a year of project management for the degree.', + cciBac2: + 'Web and mobile web developer: programming and web basics, MVC architecture, UI and UX with the Adobe suite, and an internship at House & Garden.', + }, + openSource: { + eyebrow: 'OPEN SOURCE', + title: 'Tools for the terminal, published and maintained.', + intro: + 'What I publish on GitHub, and what I send back to the projects I use: packages to ship gwm, and bugs reported upstream.', + projectsLabel: 'Maintained projects', + upstreamLabel: 'Contributions', + reportsLabel: 'Reports', + lot: 'LOT', + install: 'Install:', + site: 'Site', + repo: 'Repository', + viewAll: 'See everything on GitHub', + status: { + released: 'Released', + inDevelopment: 'In development', + archived: 'Archived', + merged: 'Merged', + review: 'In review', + report: 'Issue', + }, + projects: { + gwm: 'Git worktree manager for the terminal, CLI and TUI in Rust: it creates the worktree, runs the project setup, links the issue and shows which AI agent is working where. A single binary, no git CLI needed.', + vellum: + 'TUI SQL client: browse, query and edit a database in the terminal, with a GitHub-like diff for every write. Rust and ratatui, local-first.', + lazycurl: 'TUI HTTP client in Go, in the spirit of lazygit.', + herdrPluginGwm: + 'herdr plugin that drives gwm for worktree management: gwm stays the source of truth, herdr follows.', + claudeCrosstalk: + 'A Claude mod to read and join the conversations between several Claude Code sessions.', + kbrdnDocs: 'Multi-product documentation monorepo, Astro Starlight and Bun workspaces.', + }, + upstream: { + winget: "gwm's manifest in the winget repository", + aqua: 'gwm added to the aqua registry', + awesomeRatatui: 'gwm in the awesome-ratatui list', + nixpkgs: 'The gwm package for nixpkgs', + snacks: 'gh_start_review fails with HTTP 422 when headRefOid is empty', + codexbar: 'Helium browser cookies are not found', + terminalBrowser: 'Let a TUI host provide a target rectangle instead of a whole pane', + elasticlens: 'Laravel 13 support blocked by a pdphilip/omniterm constraint', + }, + }, + availability: 'A project, a role, a question: a message is enough.', + teaser: 'Experience, education and open source: the details are on the journey page.', + teaserCta: 'See the journey', + }, + + // /apps + appsPage: { + seoTitle: 'Apps · Kylian Bardini', + seoDescription: + "Kylian Bardini's terminal apps: gwm (git worktree manager), vellum (TUI SQL client, in development) and LazyCurl (TUI HTTP client, archived).", + ogTitle: 'Apps', + ogSubtitle: 'gwm, vellum and LazyCurl', + eyebrow: 'APPS', + title: 'Tools for the terminal.', + intro: + "What I ship, what I'm building and what I've put away. All open source, the code is on GitHub.", + site: 'Site', + repo: 'Repository', + releases: 'Releases', + install: 'Install', + status: { + released: 'Available', + inDevelopment: 'In development', + archived: 'Archived', + }, + apps: { + gwm: { + illustration: + "gwm's TUI: the repository's worktrees with their branch and state, the selected worktree's details and its latest commits", + tagline: 'Git worktree manager for the terminal, CLI and TUI.', + description: + 'gwm creates the worktree, runs the project setup, links the issue and shows which AI agent is working where. A single binary, no git CLI needed.', + features: [ + 'One command to create the worktree, set the project up and link its issue.', + 'Knows which AI agent works where: Claude Code, Codex, opencode and Mistral Vibe.', + 'A machine surface: JSON output, a JSON-RPC daemon and a statusline for your prompt.', + 'gwm undo and gwm history recover a worktree removed by mistake.', + ], + }, + vellum: { + tagline: 'TUI SQL client, with a diff for every write.', + description: + 'Browse, query and edit a database from the terminal, seeing every write as a GitHub-like diff before it applies. Pre-0.1: not usable yet.', + features: [ + 'Browse, query and edit a database without leaving the terminal.', + 'A GitHub-like diff before every write.', + 'Rust and ratatui: a single binary, no account, no telemetry.', + ], + }, + lazycurl: { + illustration: "LazyCurl's interface: collections, request and response side by side", + tagline: 'TUI HTTP client, between lazygit and Postman.', + description: "Test an API from the terminal, with lazygit's interface and Postman's tools.", + features: [ + 'Lazygit-style interface: multiple panels and vim motions.', + 'JavaScript pre- and post-request scripts, and 16 test assertions.', + 'Request collections and environment variables.', + 'cURL, OpenAPI 3 and Postman import and export.', + ], + }, + }, + }, + blogNav: { articles: 'Articles', tags: 'Tags', diff --git a/i18n/locales/fr.ts b/i18n/locales/fr.ts index 30011cb..a840fd4 100644 --- a/i18n/locales/fr.ts +++ b/i18n/locales/fr.ts @@ -50,8 +50,6 @@ export default { about: 'A PROPOS', projects: 'PROJETS', skills: 'STACK & COMPÉTENCES', - experience: 'EXPERIENCE', - education: 'FORMATION', parcours: 'PARCOURS', activity: 'ACTIVITE', blog: 'BLOG', @@ -267,6 +265,146 @@ export default { }, // Blog Navigation + // /experience + experiencePage: { + seoTitle: 'Parcours et open source · Kylian Bardini', + seoDescription: + "Le parcours de Kylian Bardini, développeur full stack : Jewely x Flippad, Virtual Immersion, Metz Numeric School, et les outils open source qu'il publie (gwm, vellum, LazyCurl).", + ogTitle: 'Parcours', + ogSubtitle: 'Expériences, formations et open source', + eyebrow: 'PARCOURS', + title: 'Développeur full stack, du code à la production.', + intro: + "Près de quatre ans en entreprise, d'abord en alternance : un ERP et des back-offices en Laravel et Symfony, des fronts en Vue, Nuxt et React, déployés sur AWS et Heroku. À côté, des outils open source pour le terminal, en Rust et en Go.", + ctaOpenSource: "Voir l'open source", + ctaBlog: 'Lire le blog', + timeline: 'Postes et formations', + experienceLabel: 'EXPÉRIENCE', + educationLabel: 'FORMATION', + stack: 'Stack', + missions: 'Missions', + skills: 'Acquis', + more: 'En savoir plus', + less: 'Réduire', + summaries: { + flippad: + "Trois ans chez Jewely x Flippad, d'abord en alternance puis en CDI : un ERP pour les bijouteries de luxe, un configurateur de bijoux multi-marques, le back-office d'Assmat-Facile et la refonte de Fiches Pédagogiques, déployés sur AWS et Heroku par GitHub Actions.", + virtualImmersion: + "Un an d'alternance chez Virtual Immersion : une API de gestion de fichiers en Symfony, de la réalité mixte sur WordPress et la navigation sur la carte d'un parc animalier en React.", + mnsMaster: + 'Ingénierie informatique : architecture logicielle avancée et micro-services, tests à tous les niveaux, intégration et déploiement continus, et deux ans de gestion de projet pour le diplôme.', + mnsLicence: + "Développeur full stack : UI et UX avec Figma, frameworks web, mobile et desktop, principes d'architecture logicielle, et un an de gestion de projet pour le diplôme.", + cciBac2: + 'Développeur web et web mobile : bases de la programmation et du web, architecture MVC, UI et UX avec la suite Adobe, et un stage chez House & Garden.', + }, + openSource: { + eyebrow: 'OPEN SOURCE', + title: 'Des outils pour le terminal, publiés et maintenus.', + intro: + "Ce que je publie sur GitHub, et ce que je remonte aux projets que j'utilise : des paquets pour distribuer gwm, et des bugs signalés en amont.", + projectsLabel: 'Projets maintenus', + upstreamLabel: 'Contributions', + reportsLabel: 'Signalements', + lot: 'LOT', + install: 'Installation :', + site: 'Site', + repo: 'Dépôt', + viewAll: 'Tout voir sur GitHub', + status: { + released: 'Publié', + inDevelopment: 'En développement', + archived: 'Archivé', + merged: 'Fusionnée', + review: 'En revue', + report: 'Issue', + }, + projects: { + gwm: "Gestionnaire de worktrees git pour le terminal, CLI et TUI en Rust : il crée le worktree, lance l'installation du projet, relie l'issue et montre quel agent IA travaille où. Un seul binaire, sans la CLI git.", + vellum: + "Client SQL en TUI : parcourir, requêter et éditer une base dans le terminal, avec un diff à la GitHub pour chaque écriture. Rust et ratatui, local d'abord.", + lazycurl: "Client HTTP en TUI, en Go, dans l'esprit de lazygit.", + herdrPluginGwm: + 'Plugin herdr qui pilote gwm pour gérer les worktrees : gwm reste la source de vérité, herdr suit.', + claudeCrosstalk: + 'Un mod Claude pour lire et rejoindre les conversations entre plusieurs sessions Claude Code.', + kbrdnDocs: 'Monorepo de documentation multi-produits, Astro Starlight et Bun workspaces.', + }, + upstream: { + winget: 'Le manifeste de gwm dans le dépôt winget', + aqua: 'gwm ajouté au registre aqua', + awesomeRatatui: 'gwm dans la liste awesome-ratatui', + nixpkgs: 'Le paquet gwm pour nixpkgs', + snacks: 'gh_start_review échoue en HTTP 422 quand headRefOid est vide', + codexbar: 'Les cookies du navigateur Helium ne sont pas trouvés', + terminalBrowser: "Laisser un hôte TUI fournir un rectangle cible plutôt qu'un pane entier", + elasticlens: 'Le support de Laravel 13 bloqué par une contrainte de pdphilip/omniterm', + }, + }, + availability: 'Un projet, un poste, une question : un message suffit.', + teaser: 'Expériences, formations et open source : le détail est sur la page parcours.', + teaserCta: 'Voir le parcours', + }, + + // /apps + appsPage: { + seoTitle: 'Apps · Kylian Bardini', + seoDescription: + 'Les apps pour le terminal de Kylian Bardini : gwm (gestionnaire de worktrees git), vellum (client SQL en TUI, en développement) et LazyCurl (client HTTP en TUI, archivé).', + ogTitle: 'Apps', + ogSubtitle: 'gwm, vellum et LazyCurl', + eyebrow: 'APPS', + title: 'Des outils pour le terminal.', + intro: + "Ce que je publie, ce que je construis et ce que j'ai rangé. Tout est open source, le code est sur GitHub.", + site: 'Site', + repo: 'Dépôt', + releases: 'Versions', + install: 'Installer', + status: { + released: 'Disponible', + inDevelopment: 'En développement', + archived: 'Archivé', + }, + apps: { + gwm: { + illustration: + 'La TUI de gwm : les worktrees du dépôt avec leur branche et leur état, le détail du worktree sélectionné et ses derniers commits', + tagline: 'Gestionnaire de worktrees git pour le terminal, CLI et TUI.', + description: + "gwm crée le worktree, lance l'installation du projet, relie l'issue et montre quel agent IA travaille où. Un seul binaire, sans la CLI git.", + features: [ + 'Une commande pour créer le worktree, installer le projet et relier son issue.', + 'Sait quel agent IA travaille où : Claude Code, Codex, opencode et Mistral Vibe.', + 'Une surface machine : sortie JSON, un daemon JSON-RPC et une ligne de statut pour le prompt.', + 'gwm undo et gwm history rattrapent un worktree supprimé par erreur.', + ], + }, + vellum: { + tagline: 'Client SQL en TUI, avec un diff pour chaque écriture.', + description: + "Parcourir, requêter et éditer une base depuis le terminal, en voyant chaque écriture comme un diff GitHub avant de l'appliquer. Pré-0.1 : pas encore utilisable.", + features: [ + 'Parcourir, requêter et éditer une base sans quitter le terminal.', + 'Un diff à la GitHub avant chaque écriture.', + 'Rust et ratatui : un seul binaire, sans compte ni télémétrie.', + ], + }, + lazycurl: { + illustration: "L'interface de LazyCurl : collections, requête et réponse côte à côte", + tagline: 'Client HTTP en TUI, entre lazygit et Postman.', + description: + "Tester une API depuis le terminal, avec l'interface de lazygit et les outils de Postman.", + features: [ + 'Interface à la lazygit : panneaux multiples et déplacements vim.', + 'Scripts JavaScript avant et après chaque requête, et 16 assertions de test.', + "Collections de requêtes et variables d'environnement.", + 'Import et export cURL, OpenAPI 3 et Postman.', + ], + }, + }, + }, + blogNav: { articles: 'Articles', tags: 'Tags', diff --git a/public/images/apps/gwm-demo.mp4 b/public/images/apps/gwm-demo.mp4 new file mode 100644 index 0000000..b961f1f Binary files /dev/null and b/public/images/apps/gwm-demo.mp4 differ diff --git a/public/images/apps/gwm.webp b/public/images/apps/gwm.webp new file mode 100644 index 0000000..194c58b Binary files /dev/null and b/public/images/apps/gwm.webp differ diff --git a/public/images/apps/lazycurl.webp b/public/images/apps/lazycurl.webp new file mode 100644 index 0000000..c1cd24b Binary files /dev/null and b/public/images/apps/lazycurl.webp differ diff --git a/server/api/github/releases.get.ts b/server/api/github/releases.get.ts new file mode 100644 index 0000000..aea82e5 --- /dev/null +++ b/server/api/github/releases.get.ts @@ -0,0 +1,45 @@ +import { createError, defineEventHandler, setResponseHeader } from 'h3' + +// Latest published release of each app on /apps, so the page never shows a stale +// version. A fixed list: the route takes no input. GraphQL needs a token (githubToken). + +const APPS = ['gwm-cli', 'vellum', 'LazyCurl'] as const + +interface ReleasesResponse { + data?: Record + errors?: Array<{ message: string }> +} + +export default defineEventHandler(async (event) => { + const token = githubToken() + if (!token) throw createError({ statusCode: 503, statusMessage: 'GitHub token not configured' }) + + // One aliased field per repository: r0, r1, … + const fields = APPS.map( + (name, i) => + `r${i}: repository(owner: "kbrdn1", name: "${name}") { latestRelease { tagName publishedAt } }`, + ).join('\n') + + const response = await $fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { Authorization: `Bearer ${token}`, 'User-Agent': 'kbrdn.dev-portfolio' }, + body: { query: `query { ${fields} }` }, + }).catch((error) => { + console.error('GitHub releases request failed:', error) + throw createError({ statusCode: 502, statusMessage: 'GitHub API error' }) + }) + + if (response.errors?.length || !response.data) { + console.error('GitHub releases errors:', response.errors) + throw createError({ statusCode: 502, statusMessage: 'GitHub API error' }) + } + + setResponseHeader(event, 'Cache-Control', 'public, max-age=3600, s-maxage=3600') + + return Object.fromEntries( + APPS.map((name, i) => { + const release = response.data![`r${i}`]?.latestRelease + return [name, release ? { version: release.tagName, publishedAt: release.publishedAt } : null] + }), + ) as Record<(typeof APPS)[number], { version: string; publishedAt: string } | null> +})