Initial release: spboucher.ai personal website
Multi-page academic + developer site (Next.js 16 App Router, Tailwind CSS v4, shadcn/ui-style components, Framer Motion): home, research, teaching, Zyquo macOS apps portfolio, CV, and a health endpoint. Dark mode, responsive, accessible. Deployed on node m2u64 (PM2, port 3100) behind ngrok at https://www.spboucher.ai. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
33 changed files +3,881 −0
added
.gitignore
+31 −0
@@ -0,0 +1,31 @@ | ||
| 1 | +# .gitignore | |
| 2 | +# spboucher.ai Web | |
| 3 | +# Author: Simon-Pierre Boucher | |
| 4 | +# Mail: contact@spboucher.ai | |
| 5 | + | |
| 6 | +# dependencies | |
| 7 | +/node_modules | |
| 8 | + | |
| 9 | +# next.js | |
| 10 | +/.next/ | |
| 11 | +/out/ | |
| 12 | + | |
| 13 | +# production | |
| 14 | +/build | |
| 15 | + | |
| 16 | +# misc | |
| 17 | +.DS_Store | |
| 18 | +*.pem | |
| 19 | + | |
| 20 | +# debug | |
| 21 | +npm-debug.log* | |
| 22 | + | |
| 23 | +# env files | |
| 24 | +.env* | |
| 25 | + | |
| 26 | +# typescript | |
| 27 | +*.tsbuildinfo | |
| 28 | +next-env.d.ts | |
| 29 | + | |
| 30 | +# local tooling settings | |
| 31 | +.claude/settings.local.json | |
added
CLAUDE.md
+191 −0
@@ -0,0 +1,191 @@ | ||
| 1 | +<!-- | |
| 2 | + CLAUDE.md | |
| 3 | + spboucher.ai — Personal Website | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +--> | |
| 7 | + | |
| 8 | +# CLAUDE.md — Personal Website (www.spboucher.ai) | |
| 9 | + | |
| 10 | +This file provides guidance to Claude Code when working on this repository. | |
| 11 | + | |
| 12 | +## Project Overview | |
| 13 | + | |
| 14 | +Personal academic and developer website for **Simon-Pierre Boucher**, Professor in the Department of Administrative Sciences at Université du Québec en Outaouais (UQO). The site presents his academic profile (research, publications, teaching) and a portfolio of macOS applications he develops (the Zyquo suite). | |
| 15 | + | |
| 16 | +- **Domain:** www.spboucher.ai | |
| 17 | +- **Hosting:** Served locally on node `m2u64` and exposed publicly via **ngrok** | |
| 18 | +- **Stack:** React / Next.js, Tailwind CSS, shadcn/ui, Framer Motion | |
| 19 | +- **Language of the site:** English | |
| 20 | + | |
| 21 | +## Mandatory File Header Convention | |
| 22 | + | |
| 23 | +**Every source file created or modified in this project (`.tsx`, `.ts`, `.jsx`, `.js`, `.css`, `.md`, config files, etc.) MUST begin with a header comment in this exact format:** | |
| 24 | + | |
| 25 | +``` | |
| 26 | +<!-- | |
| 27 | + FILENAME.ext | |
| 28 | + spboucher.ai Web | |
| 29 | + Author: Simon-Pierre Boucher | |
| 30 | + Mail: contact@spboucher.ai | |
| 31 | +--> | |
| 32 | +``` | |
| 33 | + | |
| 34 | +Adapt the comment syntax to the file type: | |
| 35 | + | |
| 36 | +- **HTML / Markdown / MDX:** `<!-- ... -->` | |
| 37 | +- **TS / TSX / JS / JSX / CSS:** | |
| 38 | + ```ts | |
| 39 | + /* | |
| 40 | + FILENAME.tsx | |
| 41 | + spboucher.ai Web | |
| 42 | + Author: Simon-Pierre Boucher | |
| 43 | + Mail: contact@spboucher.ai | |
| 44 | + */ | |
| 45 | + ``` | |
| 46 | +- **Shell / YAML / Python:** `#` prefixed lines with the same four fields. | |
| 47 | + | |
| 48 | +Never omit this header. When editing an existing file that lacks it, add it. | |
| 49 | + | |
| 50 | +## Dev & Deployment | |
| 51 | + | |
| 52 | +```bash | |
| 53 | +npm install # install dependencies | |
| 54 | +npm run dev # local dev server (default: http://localhost:3000) | |
| 55 | +npm run build # production build | |
| 56 | +npm run start # serve production build | |
| 57 | +``` | |
| 58 | + | |
| 59 | +### ngrok exposure (node m2u64) | |
| 60 | + | |
| 61 | +The site runs on node `m2u64` and is exposed with ngrok bound to the custom domain: | |
| 62 | + | |
| 63 | +```bash | |
| 64 | +ngrok http --domain=www.spboucher.ai 3000 | |
| 65 | +``` | |
| 66 | + | |
| 67 | +- Keep the production build (`npm run build && npm run start`) behind ngrok, not the dev server. | |
| 68 | +- If the tunnel drops, restart the ngrok command above on m2u64. | |
| 69 | + | |
| 70 | +## Site Structure | |
| 71 | + | |
| 72 | +``` | |
| 73 | +/ Home (hero, bio, highlights) | |
| 74 | +/research Publications, working papers, presentations | |
| 75 | +/teaching Courses taught | |
| 76 | +/apps Zyquo macOS app portfolio | |
| 77 | +/cv CV summary + contact | |
| 78 | +``` | |
| 79 | + | |
| 80 | +## Homepage Content (use this copy) | |
| 81 | + | |
| 82 | +### Hero | |
| 83 | + | |
| 84 | +- **Name:** Simon-Pierre Boucher | |
| 85 | +- **Title:** Professor — Department of Administrative Sciences | |
| 86 | +- **Institution:** Université du Québec en Outaouais (UQO), Gatineau — Pavillon Alexandre-Taché | |
| 87 | +- **Tagline suggestion:** "Financial econometrics researcher and macOS/AI developer." | |
| 88 | +- **Contact:** | |
| 89 | + - Academic: simon-pierre.boucher@uqo.ca | |
| 90 | + - Personal: contact@spboucher.ai | |
| 91 | + - GitHub: https://github.com/spboucher-ai | |
| 92 | +- **Languages:** Bilingual — French and English | |
| 93 | + | |
| 94 | +### About / Bio | |
| 95 | + | |
| 96 | +Simon-Pierre Boucher is a Professor in the Department of Administrative Sciences at Université du Québec en Outaouais (UQO). His research focuses on financial econometrics, commodity markets, monetary policy announcements, high-frequency finance, volatility modelling, textual analysis, and financialization. He is completing a Ph.D. in Business Administration (Finance and Insurance) at Université Laval under the supervision of Prof. Marie-Hélène Gagnon and Prof. Gabriel Power. Alongside his academic work, he builds native macOS applications focused on AI and local LLMs (the Zyquo suite). | |
| 97 | + | |
| 98 | +### Education | |
| 99 | + | |
| 100 | +- **Ph.D. in Business Administration (Finance and Insurance)** — Université Laval, 2020–Present | |
| 101 | +- **M.Sc. in Business Administration (Finance)** — Université Laval, 2017–2019 (Thesis: *Impact of Commuting Times on Residential Property Values: Evidence from the Province of Québec*) | |
| 102 | +- **B.B.A. in Business Administration (Finance)** — Université Laval, 2013–2017 | |
| 103 | + | |
| 104 | +### Research Highlights | |
| 105 | + | |
| 106 | +**Publication:** | |
| 107 | +- Boucher, S.-P., Gagnon, M.-H., & Power, G. J. (2025). *Speculative Trading in Energy Markets: Evidence from Macroeconomic Surprises.* The Energy Journal. | |
| 108 | + | |
| 109 | +**Working papers:** | |
| 110 | +1. *Has Financialization Changed the Impact of Macro Announcements on U.S. Commodity Markets?* — SSRN Working Paper, May 2022 — https://ssrn.com/abstract=400929 | |
| 111 | +2. *Modelling Volatility Dynamics Between Commodity ETFs and Their Net Asset Value using BVAR and HAR Models* — January 2023 | |
| 112 | +3. *Returns and Volatility Around FOMC Announcements: A High-Frequency Analysis of Policy Tone and Novelty* — November 2023 | |
| 113 | + | |
| 114 | +**Conference presentations:** CRREP Research Day (2022, 2023); Canadian Economics Association 61st & 62nd Annual Conferences (2022, 2023); 7th Winter Workshop on Commodity Markets, Mont-Tremblant (2024). | |
| 115 | + | |
| 116 | +### Teaching (Université Laval — Lecturer, 2021–Present) | |
| 117 | + | |
| 118 | +| Code | Course | Level | Terms | | |
| 119 | +|------|--------|-------|-------| | |
| 120 | +| GSF-3100 | Capital Markets | Undergraduate | F2021, W2021, F2022, W2023 | | |
| 121 | +| GSF-6053 | Financial Econometrics I | Graduate | W2022, W2025 | | |
| 122 | +| GSF-1500 | Financial Management | Undergraduate | S2022 | | |
| 123 | +| GSF-6028 | Financial Theory | Graduate | W2024 | | |
| 124 | + | |
| 125 | +Teaching Assistant at Université Laval (2018–2021): GSF-2101 Portfolio Management, GSF-2102 Corporate Finance, GSF-6008 Corporate Finance, GSF-6025 Financial Strategies & Policies I, GSF-6028 Financial Theory. | |
| 126 | + | |
| 127 | +### Technical Skills (for a skills section) | |
| 128 | + | |
| 129 | +- **Languages & frameworks:** Python, TypeScript/JavaScript, R, MATLAB, SAS/STATA, SQL, Julia, C++, Swift/SwiftUI, LaTeX, React/Next.js, Node.js, FastAPI, Flask, Streamlit, Tailwind CSS, shadcn/ui, Framer Motion | |
| 130 | +- **Data & econometrics:** Pandas, NumPy, statsmodels, scikit-learn; VAR, BVAR, HAR, GARCH, DCC-GARCH; event studies, high-frequency analysis, realized volatility; FRED, FMP, EODHD, Yahoo Finance, World Bank, IMF, OECD | |
| 131 | +- **AI, LLMs & agents:** Claude/Anthropic SDK, OpenAI API, OpenRouter, Vercel AI SDK, Ollama, llama.cpp, GGUF, Hugging Face; embeddings, RAG, vector databases; tool-augmented agents; LoRA, QLoRA, SFT fine-tuning | |
| 132 | + | |
| 133 | +## Apps Page — Zyquo macOS Suite (REQUIRED) | |
| 134 | + | |
| 135 | +The `/apps` page is a portfolio of six native macOS applications. **Each app card MUST display: the app icon, the name, a short description, and three buttons/links (GitHub Repo, Release, Download DMG).** | |
| 136 | + | |
| 137 | +App icons: store them in `/public/icons/` (e.g. `/public/icons/zyquo-cloud.png`). If an icon file is missing, fetch it from the corresponding GitHub repo (app icon asset) or use a styled placeholder with the app's emoji until the real icon is added — but the icon slot must always be present in the layout. | |
| 138 | + | |
| 139 | +### 1. ☁️ Zyquo Cloud — Multi-provider AI chat | |
| 140 | +Native macOS chat client supporting multiple AI providers (Anthropic, OpenAI, and more) in a single unified interface. | |
| 141 | +- Repo: https://github.com/spboucher-ai/zyquo-cloud | |
| 142 | +- Release: https://github.com/spboucher-ai/zyquo-cloud/releases/tag/v1.0.0 | |
| 143 | +- DMG: https://github.com/spboucher-ai/zyquo-cloud/releases/latest/download/ZyquoCloud.dmg | |
| 144 | + | |
| 145 | +### 2. 💻 Zyquo Local — 100% local LLMs (MLX) | |
| 146 | +Run large language models entirely on-device with Apple MLX. Private, offline, no API keys required. | |
| 147 | +- Repo: https://github.com/spboucher-ai/zyquo-local | |
| 148 | +- Release: https://github.com/spboucher-ai/zyquo-local/releases/tag/v1.0.0 | |
| 149 | +- DMG: https://github.com/spboucher-ai/zyquo-local/releases/latest/download/ZyquoLocal.dmg | |
| 150 | + | |
| 151 | +### 3. 🤖 Zyquo Agent — Autonomous agent for the Mac | |
| 152 | +An autonomous AI agent that can act on your Mac: tool calling, task automation, and multi-step workflows. | |
| 153 | +- Repo: https://github.com/spboucher-ai/zyquo-agent | |
| 154 | +- Release: https://github.com/spboucher-ai/zyquo-agent/releases/tag/v1.0.0 | |
| 155 | +- DMG: https://github.com/spboucher-ai/zyquo-agent/releases/latest/download/ZyquoAgent.dmg | |
| 156 | + | |
| 157 | +### 4. 🌍 Zyquo Atlas — AI-native web browser | |
| 158 | +A web browser built around AI from the ground up: page understanding, summarization, and assisted browsing. | |
| 159 | +- Repo: https://github.com/spboucher-ai/zyquo-atlas | |
| 160 | +- Release: https://github.com/spboucher-ai/zyquo-atlas/releases/tag/v1.0.0 | |
| 161 | +- DMG: https://github.com/spboucher-ai/zyquo-atlas/releases/latest/download/ZyquoAtlas.dmg | |
| 162 | + | |
| 163 | +### 5. ⚒️ Zyquo MLX — On-device model forge | |
| 164 | +Fine-tune and quantize models directly on Apple Silicon: LoRA/QLoRA fine-tuning, quantization, and model management with MLX. | |
| 165 | +- Repo: https://github.com/spboucher-ai/zyquo-mlx | |
| 166 | +- Release: https://github.com/spboucher-ai/zyquo-mlx/releases/tag/v1.0.0 | |
| 167 | +- DMG: https://github.com/spboucher-ai/zyquo-mlx/releases/latest/download/ZyquoMLX.dmg | |
| 168 | + | |
| 169 | +### 6. 🔀 Zyquo Router — Local LLM gateway | |
| 170 | +A local gateway that routes requests across LLM providers and local models through a single unified endpoint. | |
| 171 | +- Repo: https://github.com/spboucher-ai/zyquo-router | |
| 172 | +- Release: https://github.com/spboucher-ai/zyquo-router/releases/tag/v1.0.0 | |
| 173 | +- DMG: https://github.com/spboucher-ai/zyquo-router/releases/latest/download/ZyquoRouter.dmg | |
| 174 | + | |
| 175 | +## Design Guidelines | |
| 176 | + | |
| 177 | +- Clean, professional academic aesthetic with a modern developer edge. | |
| 178 | +- Dark mode support. | |
| 179 | +- Use Tailwind CSS + shadcn/ui components; Framer Motion for subtle animations only. | |
| 180 | +- Fully responsive (mobile-first). | |
| 181 | +- App cards on `/apps` in a responsive grid (1 col mobile, 2 cols tablet, 3 cols desktop). | |
| 182 | +- Semantic HTML, accessible (alt text on all app icons). | |
| 183 | + | |
| 184 | +## Rules for Claude | |
| 185 | + | |
| 186 | +1. **Always add the file header comment** (see "Mandatory File Header Convention") at the top of every file you create or edit. | |
| 187 | +2. The entire site content is in **English**. | |
| 188 | +3. Never remove or alter the app download links (Repo / Release / DMG) — they must remain exactly as listed above. | |
| 189 | +4. Every Zyquo app card must include its icon and description. | |
| 190 | +5. Keep the ngrok/m2u64 deployment instructions in the README up to date if ports or commands change. | |
| 191 | +6. Prefer server components (Next.js App Router) where possible; client components only when interactivity requires it. | |
added
README.md
+171 −0
@@ -0,0 +1,171 @@ | ||
| 1 | +<!-- | |
| 2 | + README.md | |
| 3 | + spboucher.ai Web | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +--> | |
| 7 | + | |
| 8 | +<div align="center"> | |
| 9 | + | |
| 10 | +# 🎓 spboucher.ai | |
| 11 | + | |
| 12 | +### Personal website of **Simon-Pierre Boucher** | |
| 13 | + | |
| 14 | +*Financial econometrics researcher and macOS/AI developer.* | |
| 15 | + | |
| 16 | +Professor — Department of Administrative Sciences, Université du Québec en Outaouais (UQO) | |
| 17 | + | |
| 18 | +<br/> | |
| 19 | + | |
| 20 | +[](https://www.spboucher.ai) | |
| 21 | +[](https://nextjs.org) | |
| 22 | +[](https://react.dev) | |
| 23 | +[](https://www.typescriptlang.org) | |
| 24 | + | |
| 25 | +[](https://tailwindcss.com) | |
| 26 | +[](https://ui.shadcn.com) | |
| 27 | +[](https://www.framer.com/motion/) | |
| 28 | +[](https://pm2.keymetrics.io) | |
| 29 | +[](https://ngrok.com) | |
| 30 | +[](https://uqo.ca) | |
| 31 | + | |
| 32 | +<br/> | |
| 33 | + | |
| 34 | +**[🏠 Home](https://www.spboucher.ai)** · | |
| 35 | +**[🔬 Research](https://www.spboucher.ai/research)** · | |
| 36 | +**[📚 Teaching](https://www.spboucher.ai/teaching)** · | |
| 37 | +**[🖥️ Apps](https://www.spboucher.ai/apps)** · | |
| 38 | +**[📄 CV](https://www.spboucher.ai/cv)** | |
| 39 | + | |
| 40 | +</div> | |
| 41 | + | |
| 42 | +--- | |
| 43 | + | |
| 44 | +## 👋 About | |
| 45 | + | |
| 46 | +This repository powers **[www.spboucher.ai](https://www.spboucher.ai)** — the personal academic and developer website of Simon-Pierre Boucher. It presents his academic profile (research in financial econometrics, commodity markets, monetary policy announcements, high-frequency finance, volatility modelling, textual analysis, and financialization) alongside the **Zyquo suite**: six native macOS applications focused on AI and local LLMs. | |
| 47 | + | |
| 48 | +## ✨ Features | |
| 49 | + | |
| 50 | +- ⚡ **Next.js 16 App Router** — server components everywhere, static prerendering on every page | |
| 51 | +- 🎨 **Tailwind CSS v4 + shadcn/ui-style components** — clean academic aesthetic with a modern developer edge | |
| 52 | +- 🌗 **Dark mode** — system-aware with a manual toggle, no flash on load | |
| 53 | +- 🎬 **Framer Motion** — subtle scroll reveals, with `prefers-reduced-motion` and no-JS fallbacks | |
| 54 | +- 📱 **Fully responsive** — mobile-first, 1/2/3-column app grid | |
| 55 | +- ♿ **Accessible** — semantic HTML, alt text on all app icons, keyboard-friendly nav | |
| 56 | +- 💚 **Health endpoint** — `GET /api/health` for uptime monitoring | |
| 57 | + | |
| 58 | +## 🗺️ Site Map | |
| 59 | + | |
| 60 | +| Route | Content | | |
| 61 | +|-------|---------| | |
| 62 | +| `/` | Hero, bio, education, research highlight, technical skills | | |
| 63 | +| `/research` | Publications, working papers, conference presentations | | |
| 64 | +| `/teaching` | Courses taught (lecturer & teaching assistant, Université Laval) | | |
| 65 | +| `/apps` | Zyquo macOS app portfolio | | |
| 66 | +| `/cv` | CV summary + contact | | |
| 67 | +| `/api/health` | JSON health check | | |
| 68 | + | |
| 69 | +## 🖥️ The Zyquo macOS Suite | |
| 70 | + | |
| 71 | +Six native macOS applications for AI and local LLMs — Swift/SwiftUI, built for Apple Silicon. | |
| 72 | + | |
| 73 | +| | App | Description | Links | | |
| 74 | +|---|-----|-------------|-------| | |
| 75 | +| <img src="public/icons/zyquo-cloud.png" width="48" alt="Zyquo Cloud icon"/> | **☁️ Zyquo Cloud** | Multi-provider AI chat — Anthropic, OpenAI, and more in one unified interface | [Repo](https://github.com/spboucher-ai/zyquo-cloud) · [Release](https://github.com/spboucher-ai/zyquo-cloud/releases/tag/v1.0.0) · [DMG](https://github.com/spboucher-ai/zyquo-cloud/releases/latest/download/ZyquoCloud.dmg) | | |
| 76 | +| <img src="public/icons/zyquo-local.png" width="48" alt="Zyquo Local icon"/> | **💻 Zyquo Local** | 100% local LLMs with Apple MLX — private, offline, no API keys | [Repo](https://github.com/spboucher-ai/zyquo-local) · [Release](https://github.com/spboucher-ai/zyquo-local/releases/tag/v1.0.0) · [DMG](https://github.com/spboucher-ai/zyquo-local/releases/latest/download/ZyquoLocal.dmg) | | |
| 77 | +| <img src="public/icons/zyquo-agent.png" width="48" alt="Zyquo Agent icon"/> | **🤖 Zyquo Agent** | Autonomous AI agent for the Mac — tool calling, automation, multi-step workflows | [Repo](https://github.com/spboucher-ai/zyquo-agent) · [Release](https://github.com/spboucher-ai/zyquo-agent/releases/tag/v1.0.0) · [DMG](https://github.com/spboucher-ai/zyquo-agent/releases/latest/download/ZyquoAgent.dmg) | | |
| 78 | +| <img src="public/icons/zyquo-atlas.png" width="48" alt="Zyquo Atlas icon"/> | **🌍 Zyquo Atlas** | AI-native web browser — page understanding, summarization, assisted browsing | [Repo](https://github.com/spboucher-ai/zyquo-atlas) · [Release](https://github.com/spboucher-ai/zyquo-atlas/releases/tag/v1.0.0) · [DMG](https://github.com/spboucher-ai/zyquo-atlas/releases/latest/download/ZyquoAtlas.dmg) | | |
| 79 | +| <img src="public/icons/zyquo-mlx.png" width="48" alt="Zyquo MLX icon"/> | **⚒️ Zyquo MLX** | On-device model forge — LoRA/QLoRA fine-tuning, quantization, model management | [Repo](https://github.com/spboucher-ai/zyquo-mlx) · [Release](https://github.com/spboucher-ai/zyquo-mlx/releases/tag/v1.0.0) · [DMG](https://github.com/spboucher-ai/zyquo-mlx/releases/latest/download/ZyquoMLX.dmg) | | |
| 80 | +| <img src="public/icons/zyquo-router.png" width="48" alt="Zyquo Router icon"/> | **🔀 Zyquo Router** | Local LLM gateway — one unified endpoint across providers and local models | [Repo](https://github.com/spboucher-ai/zyquo-router) · [Release](https://github.com/spboucher-ai/zyquo-router/releases/tag/v1.0.0) · [DMG](https://github.com/spboucher-ai/zyquo-router/releases/latest/download/ZyquoRouter.dmg) | | |
| 81 | + | |
| 82 | +## 🚀 Quick Start | |
| 83 | + | |
| 84 | +```bash | |
| 85 | +git clone https://github.com/spboucher-ai/spboucher.ai.git | |
| 86 | +cd spboucher.ai | |
| 87 | + | |
| 88 | +npm install # install dependencies | |
| 89 | +npm run dev # local dev server → http://localhost:3000 | |
| 90 | +npm run build # production build | |
| 91 | +npm run start # serve production build | |
| 92 | +npm run typecheck # TypeScript check | |
| 93 | +``` | |
| 94 | + | |
| 95 | +## 🌐 Production Deployment (node m2u64) | |
| 96 | + | |
| 97 | +The site runs on cluster node `m2u64` and is exposed publicly with **ngrok** bound to the custom domain. On m2u64 the app listens on **port 3100** (port 3000 is already in use on that node); both the app and the tunnel run under **PM2** for auto-restart. | |
| 98 | + | |
| 99 | +```mermaid | |
| 100 | +flowchart LR | |
| 101 | + A["🌍 Visitor"] --> B["🔒 ngrok edge<br/>www.spboucher.ai"] | |
| 102 | + B --> C["🖥️ m2u64 : 3100<br/>next start (PM2: spboucher)"] | |
| 103 | + C --> D["💚 /api/health"] | |
| 104 | +``` | |
| 105 | + | |
| 106 | +```bash | |
| 107 | +# on m2u64 | |
| 108 | +npm ci && npm run build | |
| 109 | +pm2 start scripts/start-production.sh --name spboucher --interpreter /bin/zsh | |
| 110 | +pm2 start "ngrok http --url=www.spboucher.ai 3100" --name spboucher-ngrok | |
| 111 | +pm2 save | |
| 112 | +``` | |
| 113 | + | |
| 114 | +- Always serve the production build (`npm run build && npm run start`) behind ngrok — never the dev server. | |
| 115 | +- If the tunnel drops, restart it: `pm2 restart spboucher-ngrok` (or run `ngrok http --url=www.spboucher.ai 3100` manually). | |
| 116 | +- Health check: `GET /api/health` → `{"status":"ok","service":"spboucher-ai"}`. | |
| 117 | + | |
| 118 | +## 📁 Project Structure | |
| 119 | + | |
| 120 | +``` | |
| 121 | +app/ | |
| 122 | +├── layout.tsx Root layout (theme init, header, footer) | |
| 123 | +├── page.tsx Home — hero, bio, education, highlight, skills | |
| 124 | +├── research/page.tsx Publications, working papers, presentations | |
| 125 | +├── teaching/page.tsx Courses taught | |
| 126 | +├── apps/page.tsx Zyquo macOS app portfolio | |
| 127 | +├── cv/page.tsx CV summary + contact | |
| 128 | +├── api/health/route.ts Health check endpoint | |
| 129 | +└── globals.css Tailwind v4 theme tokens (light/dark) | |
| 130 | + | |
| 131 | +components/ | |
| 132 | +├── site-header.tsx Sticky nav + mobile menu | |
| 133 | +├── site-footer.tsx Footer | |
| 134 | +├── theme-toggle.tsx Dark mode toggle | |
| 135 | +├── reveal.tsx Framer Motion scroll reveal | |
| 136 | +├── app-card.tsx Zyquo app card (icon, description, 3 links) | |
| 137 | +└── ui/ shadcn/ui-style primitives (button, card, badge) | |
| 138 | + | |
| 139 | +lib/ | |
| 140 | +├── apps.ts Zyquo suite data (single source of truth) | |
| 141 | +└── utils.ts cn() class merger | |
| 142 | + | |
| 143 | +public/icons/ Zyquo app icons (fetched from the app repos) | |
| 144 | +scripts/ Production start script (PM2) | |
| 145 | +``` | |
| 146 | + | |
| 147 | +## 📐 Conventions | |
| 148 | + | |
| 149 | +- 📝 Every source file starts with the mandatory project header comment (see `CLAUDE.md`) | |
| 150 | +- 🇬🇧 All site content is in English | |
| 151 | +- 🔗 App download links on `/apps` (Repo / Release / DMG) must remain exactly as specified in `CLAUDE.md` | |
| 152 | +- 🧩 Server components by default; client components only where interactivity requires it | |
| 153 | + | |
| 154 | +## 📫 Contact | |
| 155 | + | |
| 156 | +| | | | |
| 157 | +|---|---| | |
| 158 | +| 🎓 Academic | [simon-pierre.boucher@uqo.ca](mailto:simon-pierre.boucher@uqo.ca) | | |
| 159 | +| ✉️ Personal | [contact@spboucher.ai](mailto:contact@spboucher.ai) | | |
| 160 | +| 🐙 GitHub | [@spboucher-ai](https://github.com/spboucher-ai) | | |
| 161 | +| 📍 Office | UQO, Gatineau — Pavillon Alexandre-Taché | | |
| 162 | + | |
| 163 | +--- | |
| 164 | + | |
| 165 | +<div align="center"> | |
| 166 | + | |
| 167 | +**© Simon-Pierre Boucher** — Université du Québec en Outaouais (UQO) | |
| 168 | + | |
| 169 | +*Built with Next.js · Tailwind CSS · shadcn/ui · Framer Motion — served from a Mac, tunneled to the world.* 🚇 | |
| 170 | + | |
| 171 | +</div> | |
added
app/api/health/route.ts
+12 −0
@@ -0,0 +1,12 @@ | ||
| 1 | +/* | |
| 2 | + route.ts | |
| 3 | + spboucher.ai Web | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +*/ | |
| 7 | + | |
| 8 | +import { NextResponse } from "next/server"; | |
| 9 | + | |
| 10 | +export function GET() { | |
| 11 | + return NextResponse.json({ status: "ok", service: "spboucher-ai" }); | |
| 12 | +} | |
added
app/apps/page.tsx
+42 −0
@@ -0,0 +1,42 @@ | ||
| 1 | +/* | |
| 2 | + page.tsx | |
| 3 | + spboucher.ai Web | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +*/ | |
| 7 | + | |
| 8 | +import type { Metadata } from "next"; | |
| 9 | + | |
| 10 | +import { AppCard } from "@/components/app-card"; | |
| 11 | +import { Reveal } from "@/components/reveal"; | |
| 12 | +import { zyquoApps } from "@/lib/apps"; | |
| 13 | + | |
| 14 | +export const metadata: Metadata = { | |
| 15 | + title: "Apps — Zyquo macOS Suite", | |
| 16 | + description: | |
| 17 | + "The Zyquo suite: six native macOS applications for AI and local LLMs — multi-provider chat, on-device MLX models, autonomous agents, an AI-native browser, model fine-tuning, and LLM routing.", | |
| 18 | +}; | |
| 19 | + | |
| 20 | +export default function AppsPage() { | |
| 21 | + return ( | |
| 22 | + <div className="mx-auto max-w-5xl px-4 py-16 sm:px-6"> | |
| 23 | + <Reveal> | |
| 24 | + <h1 className="text-3xl font-bold tracking-tight sm:text-4xl"> | |
| 25 | + Zyquo macOS Suite | |
| 26 | + </h1> | |
| 27 | + <p className="mt-3 max-w-2xl text-muted-foreground"> | |
| 28 | + Six native macOS applications focused on AI and local LLMs — built | |
| 29 | + with Swift/SwiftUI for Apple Silicon. | |
| 30 | + </p> | |
| 31 | + </Reveal> | |
| 32 | + | |
| 33 | + <div className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-3"> | |
| 34 | + {zyquoApps.map((app, i) => ( | |
| 35 | + <Reveal key={app.slug} delay={i * 0.06} className="h-full"> | |
| 36 | + <AppCard app={app} /> | |
| 37 | + </Reveal> | |
| 38 | + ))} | |
| 39 | + </div> | |
| 40 | + </div> | |
| 41 | + ); | |
| 42 | +} | |
added
app/cv/page.tsx
+177 −0
@@ -0,0 +1,177 @@ | ||
| 1 | +/* | |
| 2 | + page.tsx | |
| 3 | + spboucher.ai Web | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +*/ | |
| 7 | + | |
| 8 | +import type { Metadata } from "next"; | |
| 9 | +import { Github, Mail, MapPin } from "lucide-react"; | |
| 10 | + | |
| 11 | +import { Badge } from "@/components/ui/badge"; | |
| 12 | +import { | |
| 13 | + Card, | |
| 14 | + CardContent, | |
| 15 | + CardDescription, | |
| 16 | + CardHeader, | |
| 17 | + CardTitle, | |
| 18 | +} from "@/components/ui/card"; | |
| 19 | +import { Reveal } from "@/components/reveal"; | |
| 20 | + | |
| 21 | +export const metadata: Metadata = { | |
| 22 | + title: "CV", | |
| 23 | + description: | |
| 24 | + "CV summary and contact information for Simon-Pierre Boucher — Professor, Department of Administrative Sciences, Université du Québec en Outaouais (UQO).", | |
| 25 | +}; | |
| 26 | + | |
| 27 | +const positions = [ | |
| 28 | + { | |
| 29 | + role: "Professor — Department of Administrative Sciences", | |
| 30 | + org: "Université du Québec en Outaouais (UQO), Gatineau", | |
| 31 | + period: "Present", | |
| 32 | + }, | |
| 33 | + { | |
| 34 | + role: "Lecturer", | |
| 35 | + org: "Université Laval", | |
| 36 | + period: "2021–Present", | |
| 37 | + }, | |
| 38 | + { | |
| 39 | + role: "Teaching Assistant", | |
| 40 | + org: "Université Laval", | |
| 41 | + period: "2018–2021", | |
| 42 | + }, | |
| 43 | +]; | |
| 44 | + | |
| 45 | +const education = [ | |
| 46 | + { | |
| 47 | + degree: "Ph.D. in Business Administration (Finance and Insurance)", | |
| 48 | + school: "Université Laval", | |
| 49 | + period: "2020–Present", | |
| 50 | + }, | |
| 51 | + { | |
| 52 | + degree: "M.Sc. in Business Administration (Finance)", | |
| 53 | + school: "Université Laval", | |
| 54 | + period: "2017–2019", | |
| 55 | + }, | |
| 56 | + { | |
| 57 | + degree: "B.B.A. in Business Administration (Finance)", | |
| 58 | + school: "Université Laval", | |
| 59 | + period: "2013–2017", | |
| 60 | + }, | |
| 61 | +]; | |
| 62 | + | |
| 63 | +export default function CvPage() { | |
| 64 | + return ( | |
| 65 | + <div className="mx-auto max-w-5xl px-4 py-16 sm:px-6"> | |
| 66 | + <Reveal> | |
| 67 | + <h1 className="text-3xl font-bold tracking-tight sm:text-4xl">CV</h1> | |
| 68 | + <p className="mt-3 max-w-2xl text-muted-foreground"> | |
| 69 | + Professor in the Department of Administrative Sciences at Université | |
| 70 | + du Québec en Outaouais (UQO). Research in financial econometrics, | |
| 71 | + commodity markets, monetary policy announcements, high-frequency | |
| 72 | + finance, volatility modelling, textual analysis, and | |
| 73 | + financialization. | |
| 74 | + </p> | |
| 75 | + </Reveal> | |
| 76 | + | |
| 77 | + {/* Contact */} | |
| 78 | + <section className="mt-12"> | |
| 79 | + <Reveal> | |
| 80 | + <h2 className="text-xl font-semibold tracking-tight">Contact</h2> | |
| 81 | + <Card className="mt-4"> | |
| 82 | + <CardContent className="grid gap-3 p-6 text-sm sm:grid-cols-2"> | |
| 83 | + <a | |
| 84 | + href="mailto:simon-pierre.boucher@uqo.ca" | |
| 85 | + className="inline-flex items-center gap-2 text-muted-foreground transition-colors hover:text-foreground" | |
| 86 | + > | |
| 87 | + <Mail className="h-4 w-4 text-primary" aria-hidden="true" /> | |
| 88 | + simon-pierre.boucher@uqo.ca (academic) | |
| 89 | + </a> | |
| 90 | + <a | |
| 91 | + href="mailto:contact@spboucher.ai" | |
| 92 | + className="inline-flex items-center gap-2 text-muted-foreground transition-colors hover:text-foreground" | |
| 93 | + > | |
| 94 | + <Mail className="h-4 w-4 text-primary" aria-hidden="true" /> | |
| 95 | + contact@spboucher.ai (personal) | |
| 96 | + </a> | |
| 97 | + <a | |
| 98 | + href="https://github.com/spboucher-ai" | |
| 99 | + target="_blank" | |
| 100 | + rel="noopener noreferrer" | |
| 101 | + className="inline-flex items-center gap-2 text-muted-foreground transition-colors hover:text-foreground" | |
| 102 | + > | |
| 103 | + <Github className="h-4 w-4 text-primary" aria-hidden="true" /> | |
| 104 | + github.com/spboucher-ai | |
| 105 | + </a> | |
| 106 | + <span className="inline-flex items-center gap-2 text-muted-foreground"> | |
| 107 | + <MapPin className="h-4 w-4 text-primary" aria-hidden="true" /> | |
| 108 | + UQO, Gatineau — Pavillon Alexandre-Taché | |
| 109 | + </span> | |
| 110 | + </CardContent> | |
| 111 | + </Card> | |
| 112 | + </Reveal> | |
| 113 | + </section> | |
| 114 | + | |
| 115 | + {/* Positions */} | |
| 116 | + <section className="mt-12"> | |
| 117 | + <Reveal> | |
| 118 | + <h2 className="text-xl font-semibold tracking-tight">Positions</h2> | |
| 119 | + </Reveal> | |
| 120 | + <div className="mt-4 space-y-4"> | |
| 121 | + {positions.map((p, i) => ( | |
| 122 | + <Reveal key={p.role} delay={i * 0.06}> | |
| 123 | + <Card> | |
| 124 | + <CardHeader className="flex-row items-start justify-between space-y-0"> | |
| 125 | + <div> | |
| 126 | + <CardTitle className="text-base leading-snug"> | |
| 127 | + {p.role} | |
| 128 | + </CardTitle> | |
| 129 | + <CardDescription className="mt-1">{p.org}</CardDescription> | |
| 130 | + </div> | |
| 131 | + <Badge variant="outline">{p.period}</Badge> | |
| 132 | + </CardHeader> | |
| 133 | + </Card> | |
| 134 | + </Reveal> | |
| 135 | + ))} | |
| 136 | + </div> | |
| 137 | + </section> | |
| 138 | + | |
| 139 | + {/* Education */} | |
| 140 | + <section className="mt-12"> | |
| 141 | + <Reveal> | |
| 142 | + <h2 className="text-xl font-semibold tracking-tight">Education</h2> | |
| 143 | + </Reveal> | |
| 144 | + <div className="mt-4 space-y-4"> | |
| 145 | + {education.map((item, i) => ( | |
| 146 | + <Reveal key={item.degree} delay={i * 0.06}> | |
| 147 | + <Card> | |
| 148 | + <CardHeader className="flex-row items-start justify-between space-y-0"> | |
| 149 | + <div> | |
| 150 | + <CardTitle className="text-base leading-snug"> | |
| 151 | + {item.degree} | |
| 152 | + </CardTitle> | |
| 153 | + <CardDescription className="mt-1"> | |
| 154 | + {item.school} | |
| 155 | + </CardDescription> | |
| 156 | + </div> | |
| 157 | + <Badge variant="outline">{item.period}</Badge> | |
| 158 | + </CardHeader> | |
| 159 | + </Card> | |
| 160 | + </Reveal> | |
| 161 | + ))} | |
| 162 | + </div> | |
| 163 | + </section> | |
| 164 | + | |
| 165 | + {/* Languages */} | |
| 166 | + <section className="mt-12"> | |
| 167 | + <Reveal> | |
| 168 | + <h2 className="text-xl font-semibold tracking-tight">Languages</h2> | |
| 169 | + <div className="mt-4 flex gap-2"> | |
| 170 | + <Badge variant="secondary">French — Bilingual</Badge> | |
| 171 | + <Badge variant="secondary">English — Bilingual</Badge> | |
| 172 | + </div> | |
| 173 | + </Reveal> | |
| 174 | + </section> | |
| 175 | + </div> | |
| 176 | + ); | |
| 177 | +} | |
added
app/globals.css
+77 −0
@@ -0,0 +1,77 @@ | ||
| 1 | +/* | |
| 2 | + globals.css | |
| 3 | + spboucher.ai Web | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +*/ | |
| 7 | + | |
| 8 | +@import "tailwindcss"; | |
| 9 | + | |
| 10 | +@custom-variant dark (&:where(.dark, .dark *)); | |
| 11 | + | |
| 12 | +:root { | |
| 13 | + --background: oklch(0.985 0.002 247.839); | |
| 14 | + --foreground: oklch(0.208 0.042 265.755); | |
| 15 | + --card: oklch(1 0 0); | |
| 16 | + --card-foreground: oklch(0.208 0.042 265.755); | |
| 17 | + --muted: oklch(0.968 0.007 247.896); | |
| 18 | + --muted-foreground: oklch(0.446 0.043 257.281); | |
| 19 | + --primary: oklch(0.488 0.243 264.376); | |
| 20 | + --primary-foreground: oklch(0.984 0.003 247.858); | |
| 21 | + --secondary: oklch(0.929 0.013 255.508); | |
| 22 | + --secondary-foreground: oklch(0.208 0.042 265.755); | |
| 23 | + --accent: oklch(0.929 0.013 255.508); | |
| 24 | + --accent-foreground: oklch(0.208 0.042 265.755); | |
| 25 | + --border: oklch(0.869 0.022 252.894); | |
| 26 | + --ring: oklch(0.488 0.243 264.376); | |
| 27 | +} | |
| 28 | + | |
| 29 | +.dark { | |
| 30 | + --background: oklch(0.179 0.025 265.755); | |
| 31 | + --foreground: oklch(0.929 0.013 255.508); | |
| 32 | + --card: oklch(0.218 0.028 265.755); | |
| 33 | + --card-foreground: oklch(0.929 0.013 255.508); | |
| 34 | + --muted: oklch(0.255 0.03 262.881); | |
| 35 | + --muted-foreground: oklch(0.704 0.04 256.788); | |
| 36 | + --primary: oklch(0.707 0.165 254.624); | |
| 37 | + --primary-foreground: oklch(0.179 0.025 265.755); | |
| 38 | + --secondary: oklch(0.279 0.041 260.031); | |
| 39 | + --secondary-foreground: oklch(0.929 0.013 255.508); | |
| 40 | + --accent: oklch(0.279 0.041 260.031); | |
| 41 | + --accent-foreground: oklch(0.929 0.013 255.508); | |
| 42 | + --border: oklch(0.323 0.038 262.881); | |
| 43 | + --ring: oklch(0.707 0.165 254.624); | |
| 44 | +} | |
| 45 | + | |
| 46 | +@theme inline { | |
| 47 | + --color-background: var(--background); | |
| 48 | + --color-foreground: var(--foreground); | |
| 49 | + --color-card: var(--card); | |
| 50 | + --color-card-foreground: var(--card-foreground); | |
| 51 | + --color-muted: var(--muted); | |
| 52 | + --color-muted-foreground: var(--muted-foreground); | |
| 53 | + --color-primary: var(--primary); | |
| 54 | + --color-primary-foreground: var(--primary-foreground); | |
| 55 | + --color-secondary: var(--secondary); | |
| 56 | + --color-secondary-foreground: var(--secondary-foreground); | |
| 57 | + --color-accent: var(--accent); | |
| 58 | + --color-accent-foreground: var(--accent-foreground); | |
| 59 | + --color-border: var(--border); | |
| 60 | + --color-ring: var(--ring); | |
| 61 | + --font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif; | |
| 62 | +} | |
| 63 | + | |
| 64 | +body { | |
| 65 | + background-color: var(--background); | |
| 66 | + color: var(--foreground); | |
| 67 | + font-family: var(--font-sans); | |
| 68 | +} | |
| 69 | + | |
| 70 | +/* Framer Motion reveals bake opacity:0 into SSR inline styles; guarantee | |
| 71 | + visibility when animations cannot run (reduced motion or JS disabled). */ | |
| 72 | +@media (prefers-reduced-motion: reduce) { | |
| 73 | + [data-reveal] { | |
| 74 | + opacity: 1 !important; | |
| 75 | + transform: none !important; | |
| 76 | + } | |
| 77 | +} | |
added
app/layout.tsx
+71 −0
@@ -0,0 +1,71 @@ | ||
| 1 | +/* | |
| 2 | + layout.tsx | |
| 3 | + spboucher.ai Web | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +*/ | |
| 7 | + | |
| 8 | +import type { Metadata } from "next"; | |
| 9 | +import { Inter } from "next/font/google"; | |
| 10 | + | |
| 11 | +import { SiteHeader } from "@/components/site-header"; | |
| 12 | +import { SiteFooter } from "@/components/site-footer"; | |
| 13 | + | |
| 14 | +import "./globals.css"; | |
| 15 | + | |
| 16 | +const inter = Inter({ | |
| 17 | + subsets: ["latin"], | |
| 18 | + variable: "--font-inter", | |
| 19 | +}); | |
| 20 | + | |
| 21 | +export const metadata: Metadata = { | |
| 22 | + metadataBase: new URL("https://www.spboucher.ai"), | |
| 23 | + title: { | |
| 24 | + default: "Simon-Pierre Boucher — Professor & macOS/AI Developer", | |
| 25 | + template: "%s — Simon-Pierre Boucher", | |
| 26 | + }, | |
| 27 | + description: | |
| 28 | + "Simon-Pierre Boucher is a Professor in the Department of Administrative Sciences at Université du Québec en Outaouais (UQO). Financial econometrics researcher and macOS/AI developer.", | |
| 29 | + openGraph: { | |
| 30 | + title: "Simon-Pierre Boucher", | |
| 31 | + description: | |
| 32 | + "Financial econometrics researcher and macOS/AI developer. Professor at Université du Québec en Outaouais (UQO).", | |
| 33 | + url: "https://www.spboucher.ai", | |
| 34 | + siteName: "Simon-Pierre Boucher", | |
| 35 | + locale: "en_US", | |
| 36 | + type: "website", | |
| 37 | + }, | |
| 38 | +}; | |
| 39 | + | |
| 40 | +// Applies the saved (or system) theme before first paint to avoid a flash | |
| 41 | +// of the wrong color scheme. | |
| 42 | +const themeInitScript = ` | |
| 43 | +try { | |
| 44 | + var t = localStorage.getItem("theme"); | |
| 45 | + if (t === "dark" || (!t && window.matchMedia("(prefers-color-scheme: dark)").matches)) { | |
| 46 | + document.documentElement.classList.add("dark"); | |
| 47 | + } | |
| 48 | +} catch (e) {} | |
| 49 | +`; | |
| 50 | + | |
| 51 | +export default function RootLayout({ | |
| 52 | + children, | |
| 53 | +}: Readonly<{ | |
| 54 | + children: React.ReactNode; | |
| 55 | +}>) { | |
| 56 | + return ( | |
| 57 | + <html lang="en" suppressHydrationWarning> | |
| 58 | + <head> | |
| 59 | + <script dangerouslySetInnerHTML={{ __html: themeInitScript }} /> | |
| 60 | + <noscript> | |
| 61 | + <style>{`[data-reveal]{opacity:1!important;transform:none!important}`}</style> | |
| 62 | + </noscript> | |
| 63 | + </head> | |
| 64 | + <body className={`${inter.variable} flex min-h-screen flex-col antialiased`}> | |
| 65 | + <SiteHeader /> | |
| 66 | + <main className="flex-1">{children}</main> | |
| 67 | + <SiteFooter /> | |
| 68 | + </body> | |
| 69 | + </html> | |
| 70 | + ); | |
| 71 | +} | |
added
app/page.tsx
+283 −0
@@ -0,0 +1,283 @@ | ||
| 1 | +/* | |
| 2 | + page.tsx | |
| 3 | + spboucher.ai Web | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +*/ | |
| 7 | + | |
| 8 | +import Link from "next/link"; | |
| 9 | +import { ArrowRight, Github, GraduationCap, Mail } from "lucide-react"; | |
| 10 | + | |
| 11 | +import { Badge } from "@/components/ui/badge"; | |
| 12 | +import { buttonVariants } from "@/components/ui/button"; | |
| 13 | +import { | |
| 14 | + Card, | |
| 15 | + CardContent, | |
| 16 | + CardDescription, | |
| 17 | + CardHeader, | |
| 18 | + CardTitle, | |
| 19 | +} from "@/components/ui/card"; | |
| 20 | +import { Reveal } from "@/components/reveal"; | |
| 21 | +import { cn } from "@/lib/utils"; | |
| 22 | + | |
| 23 | +const education = [ | |
| 24 | + { | |
| 25 | + degree: "Ph.D. in Business Administration (Finance and Insurance)", | |
| 26 | + school: "Université Laval", | |
| 27 | + period: "2020–Present", | |
| 28 | + }, | |
| 29 | + { | |
| 30 | + degree: "M.Sc. in Business Administration (Finance)", | |
| 31 | + school: "Université Laval", | |
| 32 | + period: "2017–2019", | |
| 33 | + note: "Thesis: Impact of Commuting Times on Residential Property Values: Evidence from the Province of Québec", | |
| 34 | + }, | |
| 35 | + { | |
| 36 | + degree: "B.B.A. in Business Administration (Finance)", | |
| 37 | + school: "Université Laval", | |
| 38 | + period: "2013–2017", | |
| 39 | + }, | |
| 40 | +]; | |
| 41 | + | |
| 42 | +const skillGroups = [ | |
| 43 | + { | |
| 44 | + title: "Languages & frameworks", | |
| 45 | + skills: [ | |
| 46 | + "Python", | |
| 47 | + "TypeScript/JavaScript", | |
| 48 | + "R", | |
| 49 | + "MATLAB", | |
| 50 | + "SAS/STATA", | |
| 51 | + "SQL", | |
| 52 | + "Julia", | |
| 53 | + "C++", | |
| 54 | + "Swift/SwiftUI", | |
| 55 | + "LaTeX", | |
| 56 | + "React/Next.js", | |
| 57 | + "Node.js", | |
| 58 | + "FastAPI", | |
| 59 | + "Flask", | |
| 60 | + "Streamlit", | |
| 61 | + "Tailwind CSS", | |
| 62 | + "shadcn/ui", | |
| 63 | + "Framer Motion", | |
| 64 | + ], | |
| 65 | + }, | |
| 66 | + { | |
| 67 | + title: "Data & econometrics", | |
| 68 | + skills: [ | |
| 69 | + "Pandas", | |
| 70 | + "NumPy", | |
| 71 | + "statsmodels", | |
| 72 | + "scikit-learn", | |
| 73 | + "VAR", | |
| 74 | + "BVAR", | |
| 75 | + "HAR", | |
| 76 | + "GARCH", | |
| 77 | + "DCC-GARCH", | |
| 78 | + "Event studies", | |
| 79 | + "High-frequency analysis", | |
| 80 | + "Realized volatility", | |
| 81 | + "FRED", | |
| 82 | + "FMP", | |
| 83 | + "EODHD", | |
| 84 | + "Yahoo Finance", | |
| 85 | + "World Bank", | |
| 86 | + "IMF", | |
| 87 | + "OECD", | |
| 88 | + ], | |
| 89 | + }, | |
| 90 | + { | |
| 91 | + title: "AI, LLMs & agents", | |
| 92 | + skills: [ | |
| 93 | + "Claude/Anthropic SDK", | |
| 94 | + "OpenAI API", | |
| 95 | + "OpenRouter", | |
| 96 | + "Vercel AI SDK", | |
| 97 | + "Ollama", | |
| 98 | + "llama.cpp", | |
| 99 | + "GGUF", | |
| 100 | + "Hugging Face", | |
| 101 | + "Embeddings", | |
| 102 | + "RAG", | |
| 103 | + "Vector databases", | |
| 104 | + "Tool-augmented agents", | |
| 105 | + "LoRA", | |
| 106 | + "QLoRA", | |
| 107 | + "SFT fine-tuning", | |
| 108 | + ], | |
| 109 | + }, | |
| 110 | +]; | |
| 111 | + | |
| 112 | +export default function HomePage() { | |
| 113 | + return ( | |
| 114 | + <div className="mx-auto max-w-5xl px-4 sm:px-6"> | |
| 115 | + {/* Hero */} | |
| 116 | + <section className="py-16 sm:py-24"> | |
| 117 | + <Reveal> | |
| 118 | + <p className="text-sm font-medium text-primary"> | |
| 119 | + Professor — Department of Administrative Sciences | |
| 120 | + </p> | |
| 121 | + <h1 className="mt-3 text-4xl font-bold tracking-tight sm:text-5xl"> | |
| 122 | + Simon-Pierre Boucher | |
| 123 | + </h1> | |
| 124 | + <p className="mt-4 max-w-2xl text-lg text-muted-foreground"> | |
| 125 | + Financial econometrics researcher and macOS/AI developer. | |
| 126 | + </p> | |
| 127 | + <p className="mt-2 text-sm text-muted-foreground"> | |
| 128 | + Université du Québec en Outaouais (UQO), Gatineau — Pavillon | |
| 129 | + Alexandre-Taché · Bilingual — French and English | |
| 130 | + </p> | |
| 131 | + <div className="mt-8 flex flex-wrap gap-3"> | |
| 132 | + <Link href="/research" className={cn(buttonVariants())}> | |
| 133 | + View Research | |
| 134 | + <ArrowRight aria-hidden="true" /> | |
| 135 | + </Link> | |
| 136 | + <Link | |
| 137 | + href="/apps" | |
| 138 | + className={cn(buttonVariants({ variant: "secondary" }))} | |
| 139 | + > | |
| 140 | + Zyquo macOS Apps | |
| 141 | + </Link> | |
| 142 | + <a | |
| 143 | + href="https://github.com/spboucher-ai" | |
| 144 | + target="_blank" | |
| 145 | + rel="noopener noreferrer" | |
| 146 | + className={cn(buttonVariants({ variant: "outline" }))} | |
| 147 | + > | |
| 148 | + <Github aria-hidden="true" /> | |
| 149 | + GitHub | |
| 150 | + </a> | |
| 151 | + </div> | |
| 152 | + <div className="mt-6 flex flex-col gap-1 text-sm text-muted-foreground sm:flex-row sm:gap-6"> | |
| 153 | + <a | |
| 154 | + href="mailto:simon-pierre.boucher@uqo.ca" | |
| 155 | + className="inline-flex items-center gap-2 transition-colors hover:text-foreground" | |
| 156 | + > | |
| 157 | + <Mail className="h-4 w-4" aria-hidden="true" /> | |
| 158 | + simon-pierre.boucher@uqo.ca | |
| 159 | + </a> | |
| 160 | + <a | |
| 161 | + href="mailto:contact@spboucher.ai" | |
| 162 | + className="inline-flex items-center gap-2 transition-colors hover:text-foreground" | |
| 163 | + > | |
| 164 | + <Mail className="h-4 w-4" aria-hidden="true" /> | |
| 165 | + contact@spboucher.ai | |
| 166 | + </a> | |
| 167 | + </div> | |
| 168 | + </Reveal> | |
| 169 | + </section> | |
| 170 | + | |
| 171 | + {/* About */} | |
| 172 | + <section className="border-t border-border py-16"> | |
| 173 | + <Reveal> | |
| 174 | + <h2 className="text-2xl font-semibold tracking-tight">About</h2> | |
| 175 | + <p className="mt-4 max-w-3xl leading-relaxed text-muted-foreground"> | |
| 176 | + Simon-Pierre Boucher is a Professor in the Department of | |
| 177 | + Administrative Sciences at Université du Québec en Outaouais (UQO). | |
| 178 | + His research focuses on financial econometrics, commodity markets, | |
| 179 | + monetary policy announcements, high-frequency finance, volatility | |
| 180 | + modelling, textual analysis, and financialization. He is completing | |
| 181 | + a Ph.D. in Business Administration (Finance and Insurance) at | |
| 182 | + Université Laval under the supervision of Prof. Marie-Hélène Gagnon | |
| 183 | + and Prof. Gabriel Power. Alongside his academic work, he builds | |
| 184 | + native macOS applications focused on AI and local LLMs (the Zyquo | |
| 185 | + suite). | |
| 186 | + </p> | |
| 187 | + </Reveal> | |
| 188 | + </section> | |
| 189 | + | |
| 190 | + {/* Education */} | |
| 191 | + <section className="border-t border-border py-16"> | |
| 192 | + <Reveal> | |
| 193 | + <h2 className="text-2xl font-semibold tracking-tight">Education</h2> | |
| 194 | + </Reveal> | |
| 195 | + <div className="mt-6 grid gap-4 sm:grid-cols-3"> | |
| 196 | + {education.map((item, i) => ( | |
| 197 | + <Reveal key={item.degree} delay={i * 0.08}> | |
| 198 | + <Card className="h-full"> | |
| 199 | + <CardHeader> | |
| 200 | + <GraduationCap | |
| 201 | + className="h-5 w-5 text-primary" | |
| 202 | + aria-hidden="true" | |
| 203 | + /> | |
| 204 | + <CardTitle className="text-base leading-snug"> | |
| 205 | + {item.degree} | |
| 206 | + </CardTitle> | |
| 207 | + <CardDescription> | |
| 208 | + {item.school} · {item.period} | |
| 209 | + </CardDescription> | |
| 210 | + </CardHeader> | |
| 211 | + {item.note && ( | |
| 212 | + <CardContent> | |
| 213 | + <p className="text-sm italic text-muted-foreground"> | |
| 214 | + {item.note} | |
| 215 | + </p> | |
| 216 | + </CardContent> | |
| 217 | + )} | |
| 218 | + </Card> | |
| 219 | + </Reveal> | |
| 220 | + ))} | |
| 221 | + </div> | |
| 222 | + </section> | |
| 223 | + | |
| 224 | + {/* Research highlight */} | |
| 225 | + <section className="border-t border-border py-16"> | |
| 226 | + <Reveal> | |
| 227 | + <h2 className="text-2xl font-semibold tracking-tight"> | |
| 228 | + Research Highlight | |
| 229 | + </h2> | |
| 230 | + <Card className="mt-6"> | |
| 231 | + <CardHeader> | |
| 232 | + <Badge className="w-fit">Publication · 2025</Badge> | |
| 233 | + <CardTitle className="text-lg leading-snug"> | |
| 234 | + Speculative Trading in Energy Markets: Evidence from | |
| 235 | + Macroeconomic Surprises | |
| 236 | + </CardTitle> | |
| 237 | + <CardDescription> | |
| 238 | + Boucher, S.-P., Gagnon, M.-H., & Power, G. J. —{" "} | |
| 239 | + <em>The Energy Journal</em> | |
| 240 | + </CardDescription> | |
| 241 | + </CardHeader> | |
| 242 | + <CardContent> | |
| 243 | + <Link | |
| 244 | + href="/research" | |
| 245 | + className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline" | |
| 246 | + > | |
| 247 | + All publications, working papers & presentations | |
| 248 | + <ArrowRight className="h-4 w-4" aria-hidden="true" /> | |
| 249 | + </Link> | |
| 250 | + </CardContent> | |
| 251 | + </Card> | |
| 252 | + </Reveal> | |
| 253 | + </section> | |
| 254 | + | |
| 255 | + {/* Skills */} | |
| 256 | + <section className="border-t border-border py-16"> | |
| 257 | + <Reveal> | |
| 258 | + <h2 className="text-2xl font-semibold tracking-tight"> | |
| 259 | + Technical Skills | |
| 260 | + </h2> | |
| 261 | + </Reveal> | |
| 262 | + <div className="mt-6 grid gap-4 md:grid-cols-3"> | |
| 263 | + {skillGroups.map((group, i) => ( | |
| 264 | + <Reveal key={group.title} delay={i * 0.08}> | |
| 265 | + <Card className="h-full"> | |
| 266 | + <CardHeader> | |
| 267 | + <CardTitle className="text-base">{group.title}</CardTitle> | |
| 268 | + </CardHeader> | |
| 269 | + <CardContent className="flex flex-wrap gap-1.5"> | |
| 270 | + {group.skills.map((skill) => ( | |
| 271 | + <Badge key={skill} variant="secondary"> | |
| 272 | + {skill} | |
| 273 | + </Badge> | |
| 274 | + ))} | |
| 275 | + </CardContent> | |
| 276 | + </Card> | |
| 277 | + </Reveal> | |
| 278 | + ))} | |
| 279 | + </div> | |
| 280 | + </section> | |
| 281 | + </div> | |
| 282 | + ); | |
| 283 | +} | |
added
app/research/page.tsx
+156 −0
@@ -0,0 +1,156 @@ | ||
| 1 | +/* | |
| 2 | + page.tsx | |
| 3 | + spboucher.ai Web | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +*/ | |
| 7 | + | |
| 8 | +import type { Metadata } from "next"; | |
| 9 | +import { ExternalLink } from "lucide-react"; | |
| 10 | + | |
| 11 | +import { Badge } from "@/components/ui/badge"; | |
| 12 | +import { | |
| 13 | + Card, | |
| 14 | + CardContent, | |
| 15 | + CardDescription, | |
| 16 | + CardHeader, | |
| 17 | + CardTitle, | |
| 18 | +} from "@/components/ui/card"; | |
| 19 | +import { Reveal } from "@/components/reveal"; | |
| 20 | + | |
| 21 | +export const metadata: Metadata = { | |
| 22 | + title: "Research", | |
| 23 | + description: | |
| 24 | + "Publications, working papers, and conference presentations by Simon-Pierre Boucher — financial econometrics, commodity markets, monetary policy, and high-frequency finance.", | |
| 25 | +}; | |
| 26 | + | |
| 27 | +const workingPapers = [ | |
| 28 | + { | |
| 29 | + title: | |
| 30 | + "Has Financialization Changed the Impact of Macro Announcements on U.S. Commodity Markets?", | |
| 31 | + venue: "SSRN Working Paper", | |
| 32 | + date: "May 2022", | |
| 33 | + link: "https://ssrn.com/abstract=400929", | |
| 34 | + }, | |
| 35 | + { | |
| 36 | + title: | |
| 37 | + "Modelling Volatility Dynamics Between Commodity ETFs and Their Net Asset Value using BVAR and HAR Models", | |
| 38 | + venue: "Working Paper", | |
| 39 | + date: "January 2023", | |
| 40 | + }, | |
| 41 | + { | |
| 42 | + title: | |
| 43 | + "Returns and Volatility Around FOMC Announcements: A High-Frequency Analysis of Policy Tone and Novelty", | |
| 44 | + venue: "Working Paper", | |
| 45 | + date: "November 2023", | |
| 46 | + }, | |
| 47 | +]; | |
| 48 | + | |
| 49 | +const presentations = [ | |
| 50 | + { event: "CRREP Research Day", detail: "2022, 2023" }, | |
| 51 | + { | |
| 52 | + event: "Canadian Economics Association — 61st & 62nd Annual Conferences", | |
| 53 | + detail: "2022, 2023", | |
| 54 | + }, | |
| 55 | + { | |
| 56 | + event: "7th Winter Workshop on Commodity Markets", | |
| 57 | + detail: "Mont-Tremblant, 2024", | |
| 58 | + }, | |
| 59 | +]; | |
| 60 | + | |
| 61 | +export default function ResearchPage() { | |
| 62 | + return ( | |
| 63 | + <div className="mx-auto max-w-5xl px-4 py-16 sm:px-6"> | |
| 64 | + <Reveal> | |
| 65 | + <h1 className="text-3xl font-bold tracking-tight sm:text-4xl"> | |
| 66 | + Research | |
| 67 | + </h1> | |
| 68 | + <p className="mt-3 max-w-2xl text-muted-foreground"> | |
| 69 | + Financial econometrics, commodity markets, monetary policy | |
| 70 | + announcements, high-frequency finance, volatility modelling, textual | |
| 71 | + analysis, and financialization. | |
| 72 | + </p> | |
| 73 | + </Reveal> | |
| 74 | + | |
| 75 | + {/* Publication */} | |
| 76 | + <section className="mt-12"> | |
| 77 | + <Reveal> | |
| 78 | + <h2 className="text-xl font-semibold tracking-tight">Publication</h2> | |
| 79 | + <Card className="mt-4"> | |
| 80 | + <CardHeader> | |
| 81 | + <Badge className="w-fit">The Energy Journal · 2025</Badge> | |
| 82 | + <CardTitle className="text-lg leading-snug"> | |
| 83 | + Speculative Trading in Energy Markets: Evidence from | |
| 84 | + Macroeconomic Surprises | |
| 85 | + </CardTitle> | |
| 86 | + <CardDescription> | |
| 87 | + Boucher, S.-P., Gagnon, M.-H., & Power, G. J. (2025) | |
| 88 | + </CardDescription> | |
| 89 | + </CardHeader> | |
| 90 | + </Card> | |
| 91 | + </Reveal> | |
| 92 | + </section> | |
| 93 | + | |
| 94 | + {/* Working papers */} | |
| 95 | + <section className="mt-12"> | |
| 96 | + <Reveal> | |
| 97 | + <h2 className="text-xl font-semibold tracking-tight"> | |
| 98 | + Working Papers | |
| 99 | + </h2> | |
| 100 | + </Reveal> | |
| 101 | + <div className="mt-4 space-y-4"> | |
| 102 | + {workingPapers.map((paper, i) => ( | |
| 103 | + <Reveal key={paper.title} delay={i * 0.08}> | |
| 104 | + <Card> | |
| 105 | + <CardHeader> | |
| 106 | + <Badge variant="secondary" className="w-fit"> | |
| 107 | + {paper.venue} · {paper.date} | |
| 108 | + </Badge> | |
| 109 | + <CardTitle className="text-lg leading-snug"> | |
| 110 | + {paper.title} | |
| 111 | + </CardTitle> | |
| 112 | + </CardHeader> | |
| 113 | + {paper.link && ( | |
| 114 | + <CardContent> | |
| 115 | + <a | |
| 116 | + href={paper.link} | |
| 117 | + target="_blank" | |
| 118 | + rel="noopener noreferrer" | |
| 119 | + className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline" | |
| 120 | + > | |
| 121 | + View on SSRN | |
| 122 | + <ExternalLink className="h-4 w-4" aria-hidden="true" /> | |
| 123 | + </a> | |
| 124 | + </CardContent> | |
| 125 | + )} | |
| 126 | + </Card> | |
| 127 | + </Reveal> | |
| 128 | + ))} | |
| 129 | + </div> | |
| 130 | + </section> | |
| 131 | + | |
| 132 | + {/* Presentations */} | |
| 133 | + <section className="mt-12"> | |
| 134 | + <Reveal> | |
| 135 | + <h2 className="text-xl font-semibold tracking-tight"> | |
| 136 | + Conference Presentations | |
| 137 | + </h2> | |
| 138 | + </Reveal> | |
| 139 | + <div className="mt-4 grid gap-4 sm:grid-cols-3"> | |
| 140 | + {presentations.map((p, i) => ( | |
| 141 | + <Reveal key={p.event} delay={i * 0.08}> | |
| 142 | + <Card className="h-full"> | |
| 143 | + <CardHeader> | |
| 144 | + <CardTitle className="text-base leading-snug"> | |
| 145 | + {p.event} | |
| 146 | + </CardTitle> | |
| 147 | + <CardDescription>{p.detail}</CardDescription> | |
| 148 | + </CardHeader> | |
| 149 | + </Card> | |
| 150 | + </Reveal> | |
| 151 | + ))} | |
| 152 | + </div> | |
| 153 | + </section> | |
| 154 | + </div> | |
| 155 | + ); | |
| 156 | +} | |
added
app/teaching/page.tsx
+155 −0
@@ -0,0 +1,155 @@ | ||
| 1 | +/* | |
| 2 | + page.tsx | |
| 3 | + spboucher.ai Web | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +*/ | |
| 7 | + | |
| 8 | +import type { Metadata } from "next"; | |
| 9 | + | |
| 10 | +import { Badge } from "@/components/ui/badge"; | |
| 11 | +import { | |
| 12 | + Card, | |
| 13 | + CardContent, | |
| 14 | + CardHeader, | |
| 15 | + CardTitle, | |
| 16 | +} from "@/components/ui/card"; | |
| 17 | +import { Reveal } from "@/components/reveal"; | |
| 18 | + | |
| 19 | +export const metadata: Metadata = { | |
| 20 | + title: "Teaching", | |
| 21 | + description: | |
| 22 | + "Courses taught by Simon-Pierre Boucher at Université Laval — capital markets, financial econometrics, financial management, and financial theory.", | |
| 23 | +}; | |
| 24 | + | |
| 25 | +const courses = [ | |
| 26 | + { | |
| 27 | + code: "GSF-3100", | |
| 28 | + title: "Capital Markets", | |
| 29 | + level: "Undergraduate", | |
| 30 | + terms: "F2021, W2021, F2022, W2023", | |
| 31 | + }, | |
| 32 | + { | |
| 33 | + code: "GSF-6053", | |
| 34 | + title: "Financial Econometrics I", | |
| 35 | + level: "Graduate", | |
| 36 | + terms: "W2022, W2025", | |
| 37 | + }, | |
| 38 | + { | |
| 39 | + code: "GSF-1500", | |
| 40 | + title: "Financial Management", | |
| 41 | + level: "Undergraduate", | |
| 42 | + terms: "S2022", | |
| 43 | + }, | |
| 44 | + { | |
| 45 | + code: "GSF-6028", | |
| 46 | + title: "Financial Theory", | |
| 47 | + level: "Graduate", | |
| 48 | + terms: "W2024", | |
| 49 | + }, | |
| 50 | +]; | |
| 51 | + | |
| 52 | +const taCourses = [ | |
| 53 | + { code: "GSF-2101", title: "Portfolio Management" }, | |
| 54 | + { code: "GSF-2102", title: "Corporate Finance" }, | |
| 55 | + { code: "GSF-6008", title: "Corporate Finance" }, | |
| 56 | + { code: "GSF-6025", title: "Financial Strategies & Policies I" }, | |
| 57 | + { code: "GSF-6028", title: "Financial Theory" }, | |
| 58 | +]; | |
| 59 | + | |
| 60 | +export default function TeachingPage() { | |
| 61 | + return ( | |
| 62 | + <div className="mx-auto max-w-5xl px-4 py-16 sm:px-6"> | |
| 63 | + <Reveal> | |
| 64 | + <h1 className="text-3xl font-bold tracking-tight sm:text-4xl"> | |
| 65 | + Teaching | |
| 66 | + </h1> | |
| 67 | + <p className="mt-3 max-w-2xl text-muted-foreground"> | |
| 68 | + Lecturer at Université Laval (2021–Present) and Teaching Assistant | |
| 69 | + (2018–2021). | |
| 70 | + </p> | |
| 71 | + </Reveal> | |
| 72 | + | |
| 73 | + {/* Lecturer */} | |
| 74 | + <section className="mt-12"> | |
| 75 | + <Reveal> | |
| 76 | + <h2 className="text-xl font-semibold tracking-tight"> | |
| 77 | + Lecturer — Université Laval (2021–Present) | |
| 78 | + </h2> | |
| 79 | + <Card className="mt-4 overflow-x-auto"> | |
| 80 | + <CardContent className="p-0"> | |
| 81 | + <table className="w-full text-sm"> | |
| 82 | + <thead> | |
| 83 | + <tr className="border-b border-border text-left"> | |
| 84 | + <th scope="col" className="px-4 py-3 font-medium"> | |
| 85 | + Code | |
| 86 | + </th> | |
| 87 | + <th scope="col" className="px-4 py-3 font-medium"> | |
| 88 | + Course | |
| 89 | + </th> | |
| 90 | + <th scope="col" className="px-4 py-3 font-medium"> | |
| 91 | + Level | |
| 92 | + </th> | |
| 93 | + <th scope="col" className="px-4 py-3 font-medium"> | |
| 94 | + Terms | |
| 95 | + </th> | |
| 96 | + </tr> | |
| 97 | + </thead> | |
| 98 | + <tbody> | |
| 99 | + {courses.map((course) => ( | |
| 100 | + <tr | |
| 101 | + key={`${course.code}-${course.terms}`} | |
| 102 | + className="border-b border-border last:border-0" | |
| 103 | + > | |
| 104 | + <td className="px-4 py-3 font-mono text-xs"> | |
| 105 | + {course.code} | |
| 106 | + </td> | |
| 107 | + <td className="px-4 py-3 font-medium">{course.title}</td> | |
| 108 | + <td className="px-4 py-3"> | |
| 109 | + <Badge | |
| 110 | + variant={ | |
| 111 | + course.level === "Graduate" | |
| 112 | + ? "default" | |
| 113 | + : "secondary" | |
| 114 | + } | |
| 115 | + > | |
| 116 | + {course.level} | |
| 117 | + </Badge> | |
| 118 | + </td> | |
| 119 | + <td className="px-4 py-3 text-muted-foreground"> | |
| 120 | + {course.terms} | |
| 121 | + </td> | |
| 122 | + </tr> | |
| 123 | + ))} | |
| 124 | + </tbody> | |
| 125 | + </table> | |
| 126 | + </CardContent> | |
| 127 | + </Card> | |
| 128 | + </Reveal> | |
| 129 | + </section> | |
| 130 | + | |
| 131 | + {/* Teaching assistant */} | |
| 132 | + <section className="mt-12"> | |
| 133 | + <Reveal> | |
| 134 | + <h2 className="text-xl font-semibold tracking-tight"> | |
| 135 | + Teaching Assistant — Université Laval (2018–2021) | |
| 136 | + </h2> | |
| 137 | + </Reveal> | |
| 138 | + <div className="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> | |
| 139 | + {taCourses.map((course, i) => ( | |
| 140 | + <Reveal key={`${course.code}-${course.title}`} delay={i * 0.06}> | |
| 141 | + <Card className="h-full"> | |
| 142 | + <CardHeader> | |
| 143 | + <Badge variant="outline" className="w-fit font-mono"> | |
| 144 | + {course.code} | |
| 145 | + </Badge> | |
| 146 | + <CardTitle className="text-base">{course.title}</CardTitle> | |
| 147 | + </CardHeader> | |
| 148 | + </Card> | |
| 149 | + </Reveal> | |
| 150 | + ))} | |
| 151 | + </div> | |
| 152 | + </section> | |
| 153 | + </div> | |
| 154 | + ); | |
| 155 | +} | |
added
components/app-card.tsx
+76 −0
@@ -0,0 +1,76 @@ | ||
| 1 | +/* | |
| 2 | + app-card.tsx | |
| 3 | + spboucher.ai Web | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +*/ | |
| 7 | + | |
| 8 | +import Image from "next/image"; | |
| 9 | +import { Download, Github, Tag } from "lucide-react"; | |
| 10 | + | |
| 11 | +import type { ZyquoApp } from "@/lib/apps"; | |
| 12 | +import { buttonVariants } from "@/components/ui/button"; | |
| 13 | +import { | |
| 14 | + Card, | |
| 15 | + CardContent, | |
| 16 | + CardDescription, | |
| 17 | + CardFooter, | |
| 18 | + CardHeader, | |
| 19 | + CardTitle, | |
| 20 | +} from "@/components/ui/card"; | |
| 21 | +import { cn } from "@/lib/utils"; | |
| 22 | + | |
| 23 | +export function AppCard({ app }: { app: ZyquoApp }) { | |
| 24 | + return ( | |
| 25 | + <Card className="flex h-full flex-col transition-shadow hover:shadow-md"> | |
| 26 | + <CardHeader className="flex-row items-center gap-4 space-y-0"> | |
| 27 | + <Image | |
| 28 | + src={app.icon} | |
| 29 | + alt={`${app.name} app icon`} | |
| 30 | + width={64} | |
| 31 | + height={64} | |
| 32 | + className="h-16 w-16 shrink-0 rounded-2xl shadow-sm" | |
| 33 | + /> | |
| 34 | + <div className="min-w-0"> | |
| 35 | + <CardTitle className="text-lg"> | |
| 36 | + <span aria-hidden="true" className="mr-1.5"> | |
| 37 | + {app.emoji} | |
| 38 | + </span> | |
| 39 | + {app.name} | |
| 40 | + </CardTitle> | |
| 41 | + <CardDescription className="mt-1">{app.tagline}</CardDescription> | |
| 42 | + </div> | |
| 43 | + </CardHeader> | |
| 44 | + <CardContent className="flex-1"> | |
| 45 | + <p className="text-sm text-muted-foreground">{app.description}</p> | |
| 46 | + </CardContent> | |
| 47 | + <CardFooter className="flex flex-wrap gap-2"> | |
| 48 | + <a | |
| 49 | + href={app.repo} | |
| 50 | + target="_blank" | |
| 51 | + rel="noopener noreferrer" | |
| 52 | + className={cn(buttonVariants({ variant: "outline", size: "sm" }))} | |
| 53 | + > | |
| 54 | + <Github aria-hidden="true" /> | |
| 55 | + GitHub Repo | |
| 56 | + </a> | |
| 57 | + <a | |
| 58 | + href={app.release} | |
| 59 | + target="_blank" | |
| 60 | + rel="noopener noreferrer" | |
| 61 | + className={cn(buttonVariants({ variant: "outline", size: "sm" }))} | |
| 62 | + > | |
| 63 | + <Tag aria-hidden="true" /> | |
| 64 | + Release | |
| 65 | + </a> | |
| 66 | + <a | |
| 67 | + href={app.dmg} | |
| 68 | + className={cn(buttonVariants({ variant: "default", size: "sm" }))} | |
| 69 | + > | |
| 70 | + <Download aria-hidden="true" /> | |
| 71 | + Download DMG | |
| 72 | + </a> | |
| 73 | + </CardFooter> | |
| 74 | + </Card> | |
| 75 | + ); | |
| 76 | +} | |
added
components/reveal.tsx
+32 −0
@@ -0,0 +1,32 @@ | ||
| 1 | +/* | |
| 2 | + reveal.tsx | |
| 3 | + spboucher.ai Web | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +*/ | |
| 7 | + | |
| 8 | +"use client"; | |
| 9 | + | |
| 10 | +import * as React from "react"; | |
| 11 | +import { motion } from "framer-motion"; | |
| 12 | + | |
| 13 | +interface RevealProps { | |
| 14 | + children: React.ReactNode; | |
| 15 | + delay?: number; | |
| 16 | + className?: string; | |
| 17 | +} | |
| 18 | + | |
| 19 | +export function Reveal({ children, delay = 0, className }: RevealProps) { | |
| 20 | + return ( | |
| 21 | + <motion.div | |
| 22 | + data-reveal | |
| 23 | + className={className} | |
| 24 | + initial={{ opacity: 0, y: 16 }} | |
| 25 | + whileInView={{ opacity: 1, y: 0 }} | |
| 26 | + viewport={{ once: true, margin: "-40px" }} | |
| 27 | + transition={{ duration: 0.5, delay, ease: "easeOut" }} | |
| 28 | + > | |
| 29 | + {children} | |
| 30 | + </motion.div> | |
| 31 | + ); | |
| 32 | +} | |
added
components/site-footer.tsx
+40 −0
@@ -0,0 +1,40 @@ | ||
| 1 | +/* | |
| 2 | + site-footer.tsx | |
| 3 | + spboucher.ai Web | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +*/ | |
| 7 | + | |
| 8 | +import Link from "next/link"; | |
| 9 | + | |
| 10 | +export function SiteFooter() { | |
| 11 | + return ( | |
| 12 | + <footer className="border-t border-border"> | |
| 13 | + <div className="mx-auto flex max-w-5xl flex-col items-center justify-between gap-4 px-4 py-8 text-sm text-muted-foreground sm:flex-row sm:px-6"> | |
| 14 | + <p> | |
| 15 | + © {new Date().getFullYear()} Simon-Pierre Boucher — Université du | |
| 16 | + Québec en Outaouais (UQO) | |
| 17 | + </p> | |
| 18 | + <div className="flex items-center gap-4"> | |
| 19 | + <a | |
| 20 | + href="mailto:contact@spboucher.ai" | |
| 21 | + className="transition-colors hover:text-foreground" | |
| 22 | + > | |
| 23 | + contact@spboucher.ai | |
| 24 | + </a> | |
| 25 | + <a | |
| 26 | + href="https://github.com/spboucher-ai" | |
| 27 | + target="_blank" | |
| 28 | + rel="noopener noreferrer" | |
| 29 | + className="transition-colors hover:text-foreground" | |
| 30 | + > | |
| 31 | + GitHub | |
| 32 | + </a> | |
| 33 | + <Link href="/cv" className="transition-colors hover:text-foreground"> | |
| 34 | + CV | |
| 35 | + </Link> | |
| 36 | + </div> | |
| 37 | + </div> | |
| 38 | + </footer> | |
| 39 | + ); | |
| 40 | +} | |
added
components/site-header.tsx
+94 −0
Large diff (94 lines) is not loaded by default to keep the page fast.Load diff
added
components/theme-toggle.tsx
+45 −0
Large diff (45 lines) is not loaded by default to keep the page fast.Load diff
added
components/ui/badge.tsx
+39 −0
Large diff (39 lines) is not loaded by default to keep the page fast.Load diff
added
components/ui/button.tsx
+55 −0
Large diff (55 lines) is not loaded by default to keep the page fast.Load diff
added
components/ui/card.tsx
+77 −0
Large diff (77 lines) is not loaded by default to keep the page fast.Load diff
added
lib/apps.ts
+93 −0
Large diff (93 lines) is not loaded by default to keep the page fast.Load diff
added
lib/utils.ts
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
next.config.ts
+14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added
package-lock.json
+1835 −0
Large diff (1,835 lines) is not loaded by default to keep the page fast.Load diff
added
package.json
+31 −0
Large diff (31 lines) is not loaded by default to keep the page fast.Load diff
added
postcss.config.mjs
+14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added
public/icons/zyquo-agent.png
+0 −0
Binary file not shown.
added
public/icons/zyquo-atlas.png
+0 −0
Binary file not shown.
added
public/icons/zyquo-cloud.png
+0 −0
Binary file not shown.
added
public/icons/zyquo-local.png
+0 −0
Binary file not shown.
added
public/icons/zyquo-mlx.png
+0 −0
Binary file not shown.
added
public/icons/zyquo-router.png
+0 −0
Binary file not shown.
added
scripts/start-production.sh
+12 −0
Large diff (12 lines) is not loaded by default to keep the page fast.Load diff
added
tsconfig.json
+45 −0
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff