SPB Git

spb/spboucher.ai Public

spboucher.ai — personal website of Simon-Pierre Boucher.

TypeScript 93.4% HTML 5.5% CSS 1%

Legendary visual upgrade: refined rounded design system, sophisticated icons, custom favicon

- New IconTile / SectionHeading / PillLink components — gradient icon
  tiles, unified section headers, chip-style document links sitewide
- Custom SB monogram favicon (SVG + ICO + apple-icon) replacing the
  generic one; matching monogram in header & footer
- Ambient background auras, softer layered shadows, larger card radii
- Hero: gradient headline, refined badge, stats strip with icon tiles
- Footer redesigned as rounded glass card with nav + social buttons
- Cards/buttons/badges polished: rings, hover lifts, rounded pills for
  all slides, papers, TPs and course material links

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 5 days ago (Aug 6, 2026) parent 9d36822

Showing 19 changed files with +517 and −279

added app/apple-icon.png +0 −0

Binary file not shown.

modified app/apps/page.tsx +14 −16
@@ -6,10 +6,12 @@
6 6 */
7 7
8 8 import type { Metadata } from "next";
9 +import { Globe, Sparkles } from "lucide-react";
9 10
10 11 import { AppCard } from "@/components/app-card";
11 12 import { ProjectCard } from "@/components/project-card";
12 13 import { Reveal } from "@/components/reveal";
14 +import { SectionHeading } from "@/components/section-heading";
13 15 import { openSourceProjects, zyquoApps } from "@/lib/apps";
14 16
15 17 export const metadata: Metadata = {
@@ -22,14 +24,13 @@ export default function AppsPage() {
22 24 return (
23 25 <div className="mx-auto max-w-5xl px-4 py-16 sm:px-6">
24 26 <Reveal>
25 <p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary">Portfolio</p>
26 <h1 className="mt-2 text-3xl font-bold tracking-tight sm:text-4xl">
27 Zyquo macOS Suite
28 </h1>
29 <p className="mt-3 max-w-2xl text-muted-foreground">
30 Six native macOS applications focused on AI and local LLMs — built
31 with Swift/SwiftUI for Apple Silicon.
32 </p>
27 + <SectionHeading
28 + as="h1"
29 + eyebrow="Portfolio"
30 + title="Zyquo macOS Suite"
31 + icon={Sparkles}
32 + description="Six native macOS applications focused on AI and local LLMs — built with Swift/SwiftUI for Apple Silicon."
33 + />
33 34 </Reveal>
34 35
35 36 <div className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
@@ -41,14 +42,11 @@ export default function AppsPage() {
41 42 </div>
42 43
43 44 <Reveal className="mt-20">
44 <h2 className="text-2xl font-bold tracking-tight sm:text-3xl">
45 Web Platforms &amp; Open Source
46 </h2>
47 <p className="mt-3 max-w-2xl text-muted-foreground">
48 Open-source platforms, research code, and teaching material — from an
49 AI-powered financial analysis platform to LLM training in pure C++ on
50 Apple Silicon.
51 </p>
45 + <SectionHeading
46 + title="Web Platforms & Open Source"
47 + icon={Globe}
48 + description="Open-source platforms, research code, and teaching material — from an AI-powered financial analysis platform to LLM training in pure C++ on Apple Silicon."
49 + />
52 50 </Reveal>
53 51
54 52 <div className="mt-10 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
modified app/cv/page.tsx +30 −22
@@ -6,7 +6,15 @@
6 6 */
7 7
8 8 import type { Metadata } from "next";
9 import { Github, Mail, MapPin } from "lucide-react";
9 +import {
10 + AtSign,
11 + Briefcase,
12 + Github,
13 + GraduationCap,
14 + Languages,
15 + Mail,
16 + MapPin,
17 +} from "lucide-react";
10 18
11 19 import { Badge } from "@/components/ui/badge";
12 20 import {
@@ -16,7 +24,9 @@ import {
16 24 CardHeader,
17 25 CardTitle,
18 26 } from "@/components/ui/card";
27 +import { IconTile } from "@/components/icon-tile";
19 28 import { Reveal } from "@/components/reveal";
29 +import { SectionHeading } from "@/components/section-heading";
20 30
21 31 export const metadata: Metadata = {
22 32 title: "CV",
@@ -64,48 +74,46 @@ export default function CvPage() {
64 74 return (
65 75 <div className="mx-auto max-w-5xl px-4 py-16 sm:px-6">
66 76 <Reveal>
67 <p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary">Curriculum Vitae</p>
68 <h1 className="mt-2 text-3xl font-bold tracking-tight sm:text-4xl">CV</h1>
69 <p className="mt-3 max-w-2xl text-muted-foreground">
70 Professor in the Department of Administrative Sciences at Université
71 du Québec en Outaouais (UQO). Research in financial econometrics,
72 commodity markets, monetary policy announcements, high-frequency
73 finance, volatility modelling, textual analysis, and
74 financialization.
75 </p>
77 + <SectionHeading
78 + as="h1"
79 + eyebrow="Curriculum Vitae"
80 + title="CV"
81 + icon={Briefcase}
82 + description="Professor in the Department of Administrative Sciences at Université du Québec en Outaouais (UQO). Research in financial econometrics, commodity markets, monetary policy announcements, high-frequency finance, volatility modelling, textual analysis, and financialization."
83 + />
76 84 </Reveal>
77 85
78 86 {/* Contact */}
79 87 <section className="mt-12">
80 88 <Reveal>
81 <h2 className="text-xl font-semibold tracking-tight">Contact</h2>
89 + <SectionHeading title="Contact" icon={AtSign} />
82 90 <Card className="mt-4">
83 91 <CardContent className="grid gap-3 p-6 text-sm sm:grid-cols-2">
84 92 <a
85 93 href="mailto:simon-pierre.boucher@uqo.ca"
86 className="inline-flex items-center gap-2 text-muted-foreground transition-colors hover:text-foreground"
94 + className="inline-flex items-center gap-3 rounded-2xl border border-border/50 bg-muted/40 px-4 py-3 text-muted-foreground transition-all hover:-translate-y-px hover:border-primary/30 hover:text-foreground"
87 95 >
88 <Mail className="h-4 w-4 text-primary" aria-hidden="true" />
96 + <IconTile icon={Mail} size="sm" />
89 97 simon-pierre.boucher@uqo.ca (academic)
90 98 </a>
91 99 <a
92 100 href="mailto:contact@spboucher.ai"
93 className="inline-flex items-center gap-2 text-muted-foreground transition-colors hover:text-foreground"
101 + className="inline-flex items-center gap-3 rounded-2xl border border-border/50 bg-muted/40 px-4 py-3 text-muted-foreground transition-all hover:-translate-y-px hover:border-primary/30 hover:text-foreground"
94 102 >
95 <Mail className="h-4 w-4 text-primary" aria-hidden="true" />
103 + <IconTile icon={Mail} size="sm" />
96 104 contact@spboucher.ai (personal)
97 105 </a>
98 106 <a
99 107 href="https://github.com/spboucher-ai"
100 108 target="_blank"
101 109 rel="noopener noreferrer"
102 className="inline-flex items-center gap-2 text-muted-foreground transition-colors hover:text-foreground"
110 + className="inline-flex items-center gap-3 rounded-2xl border border-border/50 bg-muted/40 px-4 py-3 text-muted-foreground transition-all hover:-translate-y-px hover:border-primary/30 hover:text-foreground"
103 111 >
104 <Github className="h-4 w-4 text-primary" aria-hidden="true" />
112 + <IconTile icon={Github} size="sm" />
105 113 github.com/spboucher-ai
106 114 </a>
107 <span className="inline-flex items-center gap-2 text-muted-foreground">
108 <MapPin className="h-4 w-4 text-primary" aria-hidden="true" />
115 + <span className="inline-flex items-center gap-3 rounded-2xl border border-border/50 bg-muted/40 px-4 py-3 text-muted-foreground">
116 + <IconTile icon={MapPin} size="sm" />
109 117 UQO, Gatineau — Pavillon Alexandre-Taché
110 118 </span>
111 119 </CardContent>
@@ -116,7 +124,7 @@ export default function CvPage() {
116 124 {/* Positions */}
117 125 <section className="mt-12">
118 126 <Reveal>
119 <h2 className="text-xl font-semibold tracking-tight">Positions</h2>
127 + <SectionHeading title="Positions" icon={Briefcase} />
120 128 </Reveal>
121 129 <div className="mt-4 space-y-4">
122 130 {positions.map((p, i) => (
@@ -140,7 +148,7 @@ export default function CvPage() {
140 148 {/* Education */}
141 149 <section className="mt-12">
142 150 <Reveal>
143 <h2 className="text-xl font-semibold tracking-tight">Education</h2>
151 + <SectionHeading title="Education" icon={GraduationCap} />
144 152 </Reveal>
145 153 <div className="mt-4 space-y-4">
146 154 {education.map((item, i) => (
@@ -166,7 +174,7 @@ export default function CvPage() {
166 174 {/* Languages */}
167 175 <section className="mt-12">
168 176 <Reveal>
169 <h2 className="text-xl font-semibold tracking-tight">Languages</h2>
177 + <SectionHeading title="Languages" icon={Languages} />
170 178 <div className="mt-4 flex gap-2">
171 179 <Badge variant="secondary">French — Bilingual</Badge>
172 180 <Badge variant="secondary">English — Bilingual</Badge>
added app/favicon.ico +0 −0

Binary file not shown.

modified app/globals.css +24 −4
@@ -24,6 +24,8 @@
24 24 --accent-foreground: oklch(0.3 0.04 265.755);
25 25 --border: oklch(0.915 0.012 252.894);
26 26 --ring: oklch(0.54 0.19 264.376);
27 + --aura-a: oklch(0.54 0.19 264.376 / 8%);
28 + --aura-b: oklch(0.72 0.12 200 / 6%);
27 29 }
28 30
29 31 .dark {
@@ -41,6 +43,8 @@
41 43 --accent-foreground: oklch(0.93 0.01 255.508);
42 44 --border: oklch(0.29 0.03 262.881);
43 45 --ring: oklch(0.72 0.15 258);
46 + --aura-a: oklch(0.72 0.15 258 / 9%);
47 + --aura-b: oklch(0.65 0.12 200 / 5%);
44 48 }
45 49
46 50 @theme inline {
@@ -59,10 +63,13 @@
59 63 --color-border: var(--border);
60 64 --color-ring: var(--ring);
61 65 --font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
62 --shadow-soft: 0 1px 2px oklch(0.24 0.035 265.755 / 4%),
63 0 8px 24px -8px oklch(0.24 0.035 265.755 / 8%);
64 --shadow-lift: 0 2px 4px oklch(0.24 0.035 265.755 / 5%),
65 0 16px 40px -12px oklch(0.24 0.035 265.755 / 14%);
66 + --radius-4xl: 1.75rem;
67 + --shadow-soft: 0 1px 2px oklch(0.24 0.035 265.755 / 3%),
68 + 0 10px 30px -12px oklch(0.24 0.035 265.755 / 7%);
69 + --shadow-lift: 0 2px 4px oklch(0.24 0.035 265.755 / 4%),
70 + 0 20px 48px -16px oklch(0.24 0.035 265.755 / 16%);
71 + --shadow-glow: 0 0 0 1px oklch(0.54 0.19 264.376 / 6%),
72 + 0 12px 40px -8px oklch(0.54 0.19 264.376 / 18%);
66 73 }
67 74
68 75 html {
@@ -77,6 +84,19 @@ body {
77 84 text-rendering: optimizeLegibility;
78 85 }
79 86
87 +/* Ambient auras behind the whole site — quiet, fixed, and theme-aware. */
88 +body::before {
89 + content: "";
90 + position: fixed;
91 + inset: 0;
92 + z-index: -10;
93 + pointer-events: none;
94 + background:
95 + radial-gradient(56rem 30rem at 88% -6%, var(--aura-a), transparent 60%),
96 + radial-gradient(48rem 26rem at -4% 4%, var(--aura-b), transparent 55%),
97 + radial-gradient(60rem 32rem at 50% 110%, var(--aura-b), transparent 60%);
98 +}
99 +
80 100 ::selection {
81 101 background: oklch(0.54 0.19 264.376 / 18%);
82 102 }
added app/icon.svg +24 −0
@@ -0,0 +1,24 @@
1 +<!--
2 + icon.svg
3 + spboucher.ai Web
4 + Author: Simon-Pierre Boucher
5 + Mail: contact@spboucher.ai
6 +-->
7 +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
8 + <defs>
9 + <linearGradient id="g" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
10 + <stop offset="0" stop-color="#5b5bd6"/>
11 + <stop offset="0.55" stop-color="#4646c6"/>
12 + <stop offset="1" stop-color="#2f2fa8"/>
13 + </linearGradient>
14 + <linearGradient id="sheen" x1="0" y1="0" x2="0" y2="64" gradientUnits="userSpaceOnUse">
15 + <stop offset="0" stop-color="#ffffff" stop-opacity="0.22"/>
16 + <stop offset="0.5" stop-color="#ffffff" stop-opacity="0"/>
17 + </linearGradient>
18 + </defs>
19 + <rect x="2" y="2" width="60" height="60" rx="18" fill="url(#g)"/>
20 + <rect x="2" y="2" width="60" height="60" rx="18" fill="url(#sheen)"/>
21 + <text x="32" y="33.5" text-anchor="middle" dominant-baseline="central"
22 + font-family="-apple-system, 'SF Pro Display', 'Inter', 'Segoe UI', Helvetica, Arial, sans-serif"
23 + font-size="27" font-weight="700" letter-spacing="-1.5" fill="#ffffff">SB</text>
24 +</svg>
modified app/page.tsx +64 −24
@@ -6,7 +6,18 @@
6 6 */
7 7
8 8 import Link from "next/link";
9 import { ArrowRight, Github, GraduationCap, Mail } from "lucide-react";
9 +import {
10 + AppWindow,
11 + ArrowRight,
12 + BookOpenCheck,
13 + Github,
14 + GraduationCap,
15 + Mail,
16 + ScrollText,
17 + Sparkles,
18 + UserRound,
19 + Wrench,
20 +} from "lucide-react";
10 21
11 22 import { Badge } from "@/components/ui/badge";
12 23 import { buttonVariants } from "@/components/ui/button";
@@ -17,9 +28,18 @@ import {
17 28 CardHeader,
18 29 CardTitle,
19 30 } from "@/components/ui/card";
31 +import { IconTile } from "@/components/icon-tile";
20 32 import { Reveal } from "@/components/reveal";
33 +import { SectionHeading } from "@/components/section-heading";
21 34 import { cn } from "@/lib/utils";
22 35
36 +const stats = [
37 + { icon: BookOpenCheck, value: "1", label: "Peer-reviewed publication" },
38 + { icon: ScrollText, value: "8", label: "Working papers" },
39 + { icon: AppWindow, value: "9", label: "Native macOS apps" },
40 + { icon: Github, value: "30+", label: "Open-source repositories" },
41 +];
42 +
23 43 const education = [
24 44 {
25 45 degree: "Ph.D. in Business Administration (Finance and Insurance)",
@@ -122,14 +142,11 @@ export default function HomePage() {
122 142 <div className="absolute top-20 -left-20 h-64 w-64 rounded-full bg-accent blur-3xl dark:bg-primary/5" />
123 143 </div>
124 144 <Reveal>
125 <p className="inline-flex items-center gap-2 rounded-full border border-border/70 bg-card/70 px-4 py-1.5 text-sm font-medium text-primary shadow-[var(--shadow-soft)]">
126 <span
127 aria-hidden="true"
128 className="h-1.5 w-1.5 rounded-full bg-primary"
129 />
145 + <p className="inline-flex items-center gap-2 rounded-full border border-primary/20 bg-primary/5 px-4 py-1.5 text-sm font-medium text-primary shadow-[var(--shadow-soft)] backdrop-blur">
146 + <Sparkles className="h-3.5 w-3.5" aria-hidden="true" />
130 147 Professor — Department of Administrative Sciences
131 148 </p>
132 <h1 className="mt-6 text-4xl font-bold tracking-tight sm:text-6xl">
149 + <h1 className="mt-6 bg-gradient-to-br from-foreground via-foreground to-foreground/60 bg-clip-text text-4xl font-bold tracking-tight text-transparent sm:text-6xl">
133 150 Simon-Pierre Boucher
134 151 </h1>
135 152 <p className="mt-5 max-w-2xl text-lg text-muted-foreground sm:text-xl">
@@ -177,13 +194,34 @@ export default function HomePage() {
177 194 </a>
178 195 </div>
179 196 </Reveal>
197 +
198 + {/* Stats */}
199 + <Reveal delay={0.15} className="mt-14">
200 + <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
201 + {stats.map((stat) => (
202 + <div
203 + key={stat.label}
204 + className="flex items-center gap-4 rounded-[1.75rem] border border-border/60 bg-card/70 p-5 shadow-[var(--shadow-soft)] backdrop-blur transition-all duration-300 hover:-translate-y-1 hover:shadow-[var(--shadow-lift)]"
205 + >
206 + <IconTile icon={stat.icon} />
207 + <div className="min-w-0">
208 + <p className="text-2xl font-bold tracking-tight">
209 + {stat.value}
210 + </p>
211 + <p className="text-xs leading-snug text-muted-foreground">
212 + {stat.label}
213 + </p>
214 + </div>
215 + </div>
216 + ))}
217 + </div>
218 + </Reveal>
180 219 </section>
181 220
182 221 {/* About */}
183 222 <section className="border-t border-border/50 py-16 sm:py-20">
184 223 <Reveal>
185 <p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary">Profile</p>
186 <h2 className="mt-2 text-2xl font-semibold tracking-tight sm:text-3xl">About</h2>
224 + <SectionHeading eyebrow="Profile" title="About" icon={UserRound} />
187 225 <p className="mt-4 max-w-3xl leading-relaxed text-muted-foreground">
188 226 Simon-Pierre Boucher is a Professor in the Department of
189 227 Administrative Sciences at Université du Québec en Outaouais (UQO).
@@ -202,18 +240,18 @@ export default function HomePage() {
202 240 {/* Education */}
203 241 <section className="border-t border-border/50 py-16 sm:py-20">
204 242 <Reveal>
205 <p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary">Background</p>
206 <h2 className="mt-2 text-2xl font-semibold tracking-tight sm:text-3xl">Education</h2>
243 + <SectionHeading
244 + eyebrow="Background"
245 + title="Education"
246 + icon={GraduationCap}
247 + />
207 248 </Reveal>
208 249 <div className="mt-6 grid gap-4 sm:grid-cols-3">
209 250 {education.map((item, i) => (
210 251 <Reveal key={item.degree} delay={i * 0.08}>
211 <Card className="h-full">
252 + <Card className="h-full hover:-translate-y-1 hover:shadow-[var(--shadow-lift)]">
212 253 <CardHeader>
213 <GraduationCap
214 className="h-5 w-5 text-primary"
215 aria-hidden="true"
216 />
254 + <IconTile icon={GraduationCap} size="sm" className="mb-1" />
217 255 <CardTitle className="text-base leading-snug">
218 256 {item.degree}
219 257 </CardTitle>
@@ -237,10 +275,11 @@ export default function HomePage() {
237 275 {/* Research highlight */}
238 276 <section className="border-t border-border/50 py-16 sm:py-20">
239 277 <Reveal>
240 <p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary">Latest</p>
241 <h2 className="mt-2 text-2xl font-semibold tracking-tight sm:text-3xl">
242 Research Highlight
243 </h2>
278 + <SectionHeading
279 + eyebrow="Latest"
280 + title="Research Highlight"
281 + icon={BookOpenCheck}
282 + />
244 283 <Card className="mt-6">
245 284 <CardHeader>
246 285 <Badge className="w-fit">Publication · 2025</Badge>
@@ -269,10 +308,11 @@ export default function HomePage() {
269 308 {/* Skills */}
270 309 <section className="border-t border-border/50 py-16 sm:py-20">
271 310 <Reveal>
272 <p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary">Toolbox</p>
273 <h2 className="mt-2 text-2xl font-semibold tracking-tight sm:text-3xl">
274 Technical Skills
275 </h2>
311 + <SectionHeading
312 + eyebrow="Toolbox"
313 + title="Technical Skills"
314 + icon={Wrench}
315 + />
276 316 </Reveal>
277 317 <div className="mt-6 grid gap-4 md:grid-cols-3">
278 318 {skillGroups.map((group, i) => (
modified app/research/page.tsx +49 −84
@@ -6,7 +6,16 @@
6 6 */
7 7
8 8 import type { Metadata } from "next";
9 import { ExternalLink, FileText, Github } from "lucide-react";
9 +import {
10 + BookOpenCheck,
11 + ExternalLink,
12 + FileText,
13 + FlaskConical,
14 + Github,
15 + GraduationCap,
16 + Mic,
17 + ScrollText,
18 +} from "lucide-react";
10 19
11 20 import { Badge } from "@/components/ui/badge";
12 21 import {
@@ -16,7 +25,9 @@ import {
16 25 CardHeader,
17 26 CardTitle,
18 27 } from "@/components/ui/card";
28 +import { PillLink } from "@/components/pill-link";
19 29 import { Reveal } from "@/components/reveal";
30 +import { SectionHeading } from "@/components/section-heading";
20 31
21 32 export const metadata: Metadata = {
22 33 title: "Research",
@@ -141,21 +152,19 @@ export default function ResearchPage() {
141 152 return (
142 153 <div className="mx-auto max-w-5xl px-4 py-16 sm:px-6">
143 154 <Reveal>
144 <p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary">Publications & Papers</p>
145 <h1 className="mt-2 text-3xl font-bold tracking-tight sm:text-4xl">
146 Research
147 </h1>
148 <p className="mt-3 max-w-2xl text-muted-foreground">
149 Financial econometrics, commodity markets, monetary policy
150 announcements, high-frequency finance, volatility modelling, textual
151 analysis, and financialization.
152 </p>
155 + <SectionHeading
156 + as="h1"
157 + eyebrow="Publications & Papers"
158 + title="Research"
159 + icon={FlaskConical}
160 + description="Financial econometrics, commodity markets, monetary policy announcements, high-frequency finance, volatility modelling, textual analysis, and financialization."
161 + />
153 162 </Reveal>
154 163
155 164 {/* Publication */}
156 165 <section className="mt-12">
157 166 <Reveal>
158 <h2 className="text-xl font-semibold tracking-tight">Publication</h2>
167 + <SectionHeading title="Publication" icon={BookOpenCheck} />
159 168 <Card className="mt-4">
160 169 <CardHeader>
161 170 <Badge className="w-fit">The Energy Journal · 2025</Badge>
@@ -174,8 +183,8 @@ export default function ResearchPage() {
174 183 {/* PhD thesis */}
175 184 <section className="mt-12">
176 185 <Reveal>
177 <h2 className="text-xl font-semibold tracking-tight">PhD Thesis</h2>
178 <p className="mt-2 max-w-3xl text-sm text-muted-foreground">
186 + <SectionHeading title="PhD Thesis" icon={GraduationCap} />
187 + <p className="mt-3 max-w-3xl text-sm text-muted-foreground">
179 188 <em>
180 189 Three Essays on High-Frequency Return and Volatility Dynamics in
181 190 Commodities and Financial Futures Markets
@@ -183,25 +192,19 @@ export default function ResearchPage() {
183 192 — doctoral thesis by articles, Université Laval (188 pages).
184 193 Co-authors: Marie-Hélène Gagnon &amp; Gabriel J. Power.
185 194 </p>
186 <div className="mt-3 flex flex-wrap gap-3">
187 <a
195 + <div className="mt-4 flex flex-wrap gap-2">
196 + <PillLink
188 197 href="https://github.com/spboucher-ai/phd-thesis/blob/main/these-ulaval/main.pdf"
189 target="_blank"
190 rel="noopener noreferrer"
191 className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline"
198 + icon={FileText}
192 199 >
193 <FileText className="h-4 w-4" aria-hidden="true" />
194 200 Full thesis (PDF)
195 </a>
196 <a
201 + </PillLink>
202 + <PillLink
197 203 href="https://github.com/spboucher-ai/phd-thesis"
198 target="_blank"
199 rel="noopener noreferrer"
200 className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline"
204 + icon={Github}
201 205 >
202 <Github className="h-4 w-4" aria-hidden="true" />
203 206 GitHub repository
204 </a>
207 + </PillLink>
205 208 </div>
206 209 </Reveal>
207 210 <div className="mt-4 space-y-4">
@@ -216,25 +219,13 @@ export default function ResearchPage() {
216 219 {chapter.title}
217 220 </CardTitle>
218 221 </CardHeader>
219 <CardContent className="flex flex-wrap gap-4">
220 <a
221 href={chapter.pdf}
222 target="_blank"
223 rel="noopener noreferrer"
224 className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline"
225 >
226 <FileText className="h-4 w-4" aria-hidden="true" />
222 + <CardContent className="flex flex-wrap gap-2">
223 + <PillLink href={chapter.pdf} icon={FileText}>
227 224 Paper PDF
228 </a>
229 <a
230 href={chapter.source}
231 target="_blank"
232 rel="noopener noreferrer"
233 className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline"
234 >
235 <Github className="h-4 w-4" aria-hidden="true" />
225 + </PillLink>
226 + <PillLink href={chapter.source} icon={Github}>
236 227 LaTeX source
237 </a>
228 + </PillLink>
238 229 </CardContent>
239 230 </Card>
240 231 </Reveal>
@@ -245,15 +236,11 @@ export default function ResearchPage() {
245 236 {/* UQO working paper series */}
246 237 <section className="mt-12">
247 238 <Reveal>
248 <h2 className="text-xl font-semibold tracking-tight">
249 UQO Working Paper Series
250 </h2>
251 <p className="mt-2 max-w-3xl text-sm text-muted-foreground">
252 Applied econometrics research produced at the Département des
253 sciences administratives, UQO — hedonic pricing, housing markets,
254 and derivatives. Each paper is backed by a fully reproducible
255 pipeline.
256 </p>
239 + <SectionHeading
240 + title="UQO Working Paper Series"
241 + icon={ScrollText}
242 + description="Applied econometrics research produced at the Département des sciences administratives, UQO — hedonic pricing, housing markets, and derivatives. Each paper is backed by a fully reproducible pipeline."
243 + />
257 244 </Reveal>
258 245 <div className="mt-4 space-y-4">
259 246 {uqoWorkingPapers.map((paper, i) => (
@@ -268,26 +255,14 @@ export default function ResearchPage() {
268 255 </CardTitle>
269 256 <CardDescription>{paper.description}</CardDescription>
270 257 </CardHeader>
271 <CardContent className="flex flex-wrap gap-4">
272 <a
273 href={paper.pdf}
274 target="_blank"
275 rel="noopener noreferrer"
276 className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline"
277 >
278 <FileText className="h-4 w-4" aria-hidden="true" />
258 + <CardContent className="flex flex-wrap gap-2">
259 + <PillLink href={paper.pdf} icon={FileText}>
279 260 Paper PDF
280 </a>
261 + </PillLink>
281 262 {paper.repo && (
282 <a
283 href={paper.repo}
284 target="_blank"
285 rel="noopener noreferrer"
286 className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline"
287 >
288 <Github className="h-4 w-4" aria-hidden="true" />
263 + <PillLink href={paper.repo} icon={Github}>
289 264 Code &amp; data repository
290 </a>
265 + </PillLink>
291 266 )}
292 267 </CardContent>
293 268 </Card>
@@ -299,9 +274,7 @@ export default function ResearchPage() {
299 274 {/* Working papers */}
300 275 <section className="mt-12">
301 276 <Reveal>
302 <h2 className="text-xl font-semibold tracking-tight">
303 Earlier Working Papers
304 </h2>
277 + <SectionHeading title="Earlier Working Papers" icon={FlaskConical} />
305 278 </Reveal>
306 279 <div className="mt-4 space-y-4">
307 280 {workingPapers.map((paper, i) => (
@@ -317,15 +290,9 @@ export default function ResearchPage() {
317 290 </CardHeader>
318 291 {paper.link && (
319 292 <CardContent>
320 <a
321 href={paper.link}
322 target="_blank"
323 rel="noopener noreferrer"
324 className="inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline"
325 >
293 + <PillLink href={paper.link} icon={ExternalLink}>
326 294 View on SSRN
327 <ExternalLink className="h-4 w-4" aria-hidden="true" />
328 </a>
295 + </PillLink>
329 296 </CardContent>
330 297 )}
331 298 </Card>
@@ -337,14 +304,12 @@ export default function ResearchPage() {
337 304 {/* Presentations */}
338 305 <section className="mt-12">
339 306 <Reveal>
340 <h2 className="text-xl font-semibold tracking-tight">
341 Conference Presentations
342 </h2>
307 + <SectionHeading title="Conference Presentations" icon={Mic} />
343 308 </Reveal>
344 309 <div className="mt-4 grid gap-4 sm:grid-cols-3">
345 310 {presentations.map((p, i) => (
346 311 <Reveal key={p.event} delay={i * 0.08}>
347 <Card className="h-full">
312 + <Card className="h-full hover:-translate-y-1 hover:shadow-[var(--shadow-lift)]">
348 313 <CardHeader>
349 314 <CardTitle className="text-base leading-snug">
350 315 {p.event}
modified app/teaching/page.tsx +84 −90
@@ -6,7 +6,22 @@
6 6 */
7 7
8 8 import type { Metadata } from "next";
9 import { FileText, Github, Presentation, Wrench } from "lucide-react";
9 +import type { LucideIcon } from "lucide-react";
10 +import {
11 + BadgeCheck,
12 + BookOpen,
13 + FileText,
14 + Github,
15 + Library,
16 + NotebookPen,
17 + PencilLine,
18 + Presentation,
19 + School,
20 + Sigma,
21 + Terminal,
22 + Users,
23 + Wrench,
24 +} from "lucide-react";
10 25
11 26 import { Badge } from "@/components/ui/badge";
12 27 import {
@@ -16,7 +31,9 @@ import {
16 31 CardHeader,
17 32 CardTitle,
18 33 } from "@/components/ui/card";
34 +import { PillLink } from "@/components/pill-link";
19 35 import { Reveal } from "@/components/reveal";
36 +import { SectionHeading } from "@/components/section-heading";
20 37
21 38 export const metadata: Metadata = {
22 39 title: "Teaching",
@@ -113,6 +130,7 @@ const GH_RAW = "https://raw.githubusercontent.com/spboucher-ai";
113 130 interface MaterialGroup {
114 131 title: string;
115 132 kind: "pills" | "list";
133 + icon: LucideIcon;
116 134 links: { label: string; file: string; topic?: string }[];
117 135 }
118 136
@@ -137,6 +155,7 @@ const ulavalCourses: UlavalCourse[] = [
137 155 {
138 156 title: "Slides — 13 section decks (PDF)",
139 157 kind: "pills",
158 + icon: Presentation,
140 159 links: [
141 160 { label: "01", file: "Section 1/GSF3100_S01.pdf" },
142 161 { label: "02", file: "Section 2/GSF3100_S02.pdf" },
@@ -166,6 +185,7 @@ const ulavalCourses: UlavalCourse[] = [
166 185 {
167 186 title: "Slides — sessions 2–12 (PDF)",
168 187 kind: "pills",
188 + icon: Presentation,
169 189 links: Array.from({ length: 11 }, (_, i) => {
170 190 const n = i + 2;
171 191 return {
@@ -177,6 +197,7 @@ const ulavalCourses: UlavalCourse[] = [
177 197 {
178 198 title: "Stata labs (PDF)",
179 199 kind: "list",
200 + icon: Terminal,
180 201 links: [
181 202 { label: "Stata session 02", file: "STATA_S02/GSF6053_STATA_02.pdf" },
182 203 { label: "Stata session 03", file: "STATA_S03/GSF6053_STATA_03.pdf" },
@@ -195,6 +216,7 @@ const ulavalCourses: UlavalCourse[] = [
195 216 {
196 217 title: "Lecture slides (PDF)",
197 218 kind: "list",
219 + icon: Presentation,
198 220 links: [
199 221 {
200 222 label: "S1 — Introduction",
@@ -230,6 +252,7 @@ const ulavalCourses: UlavalCourse[] = [
230 252 {
231 253 title: "Lecture notes (PDF)",
232 254 kind: "list",
255 + icon: NotebookPen,
233 256 links: [
234 257 { label: "Introduction", file: "PDF/GSF6053_H25_NOTE_INTRO.pdf" },
235 258 {
@@ -262,6 +285,7 @@ const ulavalCourses: UlavalCourse[] = [
262 285 {
263 286 title: "Formal proofs (PDF)",
264 287 kind: "list",
288 + icon: Sigma,
265 289 links: [
266 290 {
267 291 label: "OLS — summation form",
@@ -290,6 +314,7 @@ const ulavalCourses: UlavalCourse[] = [
290 314 {
291 315 title: "Exercise sets 1–14 (PDF)",
292 316 kind: "pills",
317 + icon: PencilLine,
293 318 links: Array.from({ length: 14 }, (_, i) => {
294 319 const n = String(i + 1).padStart(3, "0");
295 320 return {
@@ -301,6 +326,7 @@ const ulavalCourses: UlavalCourse[] = [
301 326 {
302 327 title: "Solutions 1–14 (PDF)",
303 328 kind: "pills",
329 + icon: BadgeCheck,
304 330 links: Array.from({ length: 14 }, (_, i) => {
305 331 const n = String(i + 1).padStart(3, "0");
306 332 return {
@@ -352,37 +378,23 @@ export default function TeachingPage() {
352 378 return (
353 379 <div className="mx-auto max-w-5xl px-4 py-16 sm:px-6">
354 380 <Reveal>
355 <p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary">Courses</p>
356 <h1 className="mt-2 text-3xl font-bold tracking-tight sm:text-4xl">
357 Teaching
358 </h1>
359 <p className="mt-3 max-w-2xl text-muted-foreground">
360 Professor at Université du Québec en Outaouais (UQO), previously
361 Lecturer (2021–Present) and Teaching Assistant (2018–2021) at
362 Université Laval.
363 </p>
381 + <SectionHeading
382 + as="h1"
383 + eyebrow="Courses"
384 + title="Teaching"
385 + icon={BookOpen}
386 + description="Professor at Université du Québec en Outaouais (UQO), previously Lecturer (2021–Present) and Teaching Assistant (2018–2021) at Université Laval."
387 + />
364 388 </Reveal>
365 389
366 390 {/* UQO courses with material */}
367 391 <section className="mt-12">
368 392 <Reveal>
369 <h2 className="text-xl font-semibold tracking-tight">
370 Professor — Université du Québec en Outaouais (UQO)
371 </h2>
372 <p className="mt-2 max-w-3xl text-sm text-muted-foreground">
373 Complete course material — Beamer slides, practical assignments
374 with Excel templates, and a Québec real-estate market report — is
375 openly available on{" "}
376 <a
377 href={UQO_REPO}
378 target="_blank"
379 rel="noopener noreferrer"
380 className="font-medium text-primary hover:underline"
381 >
382 GitHub
383 </a>
384 .
385 </p>
393 + <SectionHeading
394 + title="Professor — Université du Québec en Outaouais (UQO)"
395 + icon={School}
396 + description="Complete course material — Beamer slides, practical assignments with Excel templates, and a Québec real-estate market report — is openly available on GitHub."
397 + />
386 398 </Reveal>
387 399 <div className="mt-4 grid gap-4 sm:grid-cols-2">
388 400 {uqoCourses.map((course, i) => (
@@ -396,15 +408,14 @@ export default function TeachingPage() {
396 408 <CardDescription>{course.description}</CardDescription>
397 409 </CardHeader>
398 410 <CardContent className="flex flex-1 flex-col gap-4">
399 <a
400 href={`${UQO_RAW}/${course.dir}/01_Plan_de_cours/plan_de_cours.pdf`}
401 target="_blank"
402 rel="noopener noreferrer"
403 className="inline-flex items-center gap-2 text-sm font-medium text-primary hover:underline"
404 >
405 <FileText className="h-4 w-4" aria-hidden="true" />
406 Course plan (PDF)
407 </a>
411 + <div>
412 + <PillLink
413 + href={`${UQO_RAW}/${course.dir}/01_Plan_de_cours/plan_de_cours.pdf`}
414 + icon={FileText}
415 + >
416 + Course plan (PDF)
417 + </PillLink>
418 + </div>
408 419
409 420 <div>
410 421 <p className="inline-flex items-center gap-2 text-sm font-medium">
@@ -423,7 +434,7 @@ export default function TeachingPage() {
423 434 rel="noopener noreferrer"
424 435 title={slide.topic}
425 436 aria-label={`Session ${slide.label}${slide.topic ? ` — ${slide.topic}` : ""} (PDF)`}
426 className="inline-flex h-8 w-9 items-center justify-center rounded-full border border-border/70 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/40 hover:bg-accent hover:text-primary"
437 + className="inline-flex h-8 w-9 items-center justify-center rounded-full border border-border/70 bg-card/70 text-xs font-medium text-muted-foreground shadow-sm transition-all hover:-translate-y-px hover:border-primary/40 hover:bg-primary/10 hover:text-primary"
427 438 >
428 439 {slide.label}
429 440 </a>
@@ -439,17 +450,14 @@ export default function TeachingPage() {
439 450 />
440 451 Practical assignments (PDF)
441 452 </p>
442 <div className="mt-2 flex flex-col gap-1.5">
453 + <div className="mt-2 flex flex-wrap gap-1.5">
443 454 {course.tps.map((tp) => (
444 <a
455 + <PillLink
445 456 key={tp.file}
446 457 href={`${UQO_RAW}/${course.dir}/${tp.file}`}
447 target="_blank"
448 rel="noopener noreferrer"
449 className="text-sm text-primary hover:underline"
450 458 >
451 459 {tp.label}
452 </a>
460 + </PillLink>
453 461 ))}
454 462 </div>
455 463 </div>
@@ -459,25 +467,16 @@ export default function TeachingPage() {
459 467 ))}
460 468 </div>
461 469 <Reveal className="mt-4">
462 <div className="flex flex-wrap gap-4">
463 <a
470 + <div className="flex flex-wrap gap-2">
471 + <PillLink
464 472 href={`${UQO_RAW}/Rapport_Immobilier_Quebec/rapport_immobilier_quebec.pdf`}
465 target="_blank"
466 rel="noopener noreferrer"
467 className="inline-flex items-center gap-2 text-sm font-medium text-primary hover:underline"
473 + icon={FileText}
468 474 >
469 <FileText className="h-4 w-4" aria-hidden="true" />
470 475 Québec real-estate market report (PDF)
471 </a>
472 <a
473 href={UQO_REPO}
474 target="_blank"
475 rel="noopener noreferrer"
476 className="inline-flex items-center gap-2 text-sm font-medium text-primary hover:underline"
477 >
478 <Github className="h-4 w-4" aria-hidden="true" />
476 + </PillLink>
477 + <PillLink href={UQO_REPO} icon={Github}>
479 478 Full course repository
480 </a>
479 + </PillLink>
481 480 </div>
482 481 </Reveal>
483 482 </section>
@@ -485,9 +484,10 @@ export default function TeachingPage() {
485 484 {/* Lecturer */}
486 485 <section className="mt-12">
487 486 <Reveal>
488 <h2 className="text-xl font-semibold tracking-tight">
489 Lecturer — Université Laval (2021–Present)
490 </h2>
487 + <SectionHeading
488 + title="Lecturer — Université Laval (2021–Present)"
489 + icon={BookOpen}
490 + />
491 491 <Card className="mt-4 overflow-x-auto">
492 492 <CardContent className="p-0">
493 493 <table className="w-full text-sm">
@@ -540,15 +540,12 @@ export default function TeachingPage() {
540 540 </Reveal>
541 541
542 542 <Reveal className="mt-8">
543 <h3 className="text-lg font-semibold tracking-tight">
544 Course Material — Université Laval
545 </h3>
546 <p className="mt-2 max-w-3xl text-sm text-muted-foreground">
547 Complete material from courses previously taught at Université
548 Laval — LaTeX Beamer slides, lecture notes, formal proofs, Stata
549 labs, and exercise sets with solutions — openly available on
550 GitHub.
551 </p>
543 + <SectionHeading
544 + as="h3"
545 + title="Course Material — Université Laval"
546 + icon={Library}
547 + description="Complete material from courses previously taught at Université Laval — LaTeX Beamer slides, lecture notes, formal proofs, Stata labs, and exercise sets with solutions — openly available on GitHub."
548 + />
552 549 </Reveal>
553 550 <div className="mt-4 space-y-4">
554 551 {ulavalCourses.map((course, i) => (
@@ -570,7 +567,7 @@ export default function TeachingPage() {
570 567 {course.groups.map((group) => (
571 568 <div key={group.title}>
572 569 <p className="inline-flex items-center gap-2 text-sm font-medium">
573 <Presentation
570 + <group.icon
574 571 className="h-4 w-4 text-primary"
575 572 aria-hidden="true"
576 573 />
@@ -587,40 +584,36 @@ export default function TeachingPage() {
587 584 target="_blank"
588 585 rel="noopener noreferrer"
589 586 aria-label={`${course.code} — ${group.title} — ${link.label} (PDF)`}
590 className="inline-flex h-8 min-w-9 items-center justify-center rounded-full border border-border/70 px-2 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/40 hover:bg-accent hover:text-primary"
587 + className="inline-flex h-8 min-w-9 items-center justify-center rounded-full border border-border/70 bg-card/70 px-2 text-xs font-medium text-muted-foreground shadow-sm transition-all hover:-translate-y-px hover:border-primary/40 hover:bg-primary/10 hover:text-primary"
591 588 >
592 589 {link.label}
593 590 </a>
594 591 ))}
595 592 </div>
596 593 ) : (
597 <div className="mt-2 flex flex-wrap gap-x-5 gap-y-1.5">
594 + <div className="mt-2 flex flex-wrap gap-1.5">
598 595 {group.links.map((link) => (
599 <a
596 + <PillLink
600 597 key={link.file}
601 598 href={encodeURI(
602 599 `${GH_RAW}/${course.repoName}/main/${link.file}`,
603 600 )}
604 target="_blank"
605 rel="noopener noreferrer"
606 className="text-sm text-primary hover:underline"
607 601 >
608 602 {link.label}
609 </a>
603 + </PillLink>
610 604 ))}
611 605 </div>
612 606 )}
613 607 </div>
614 608 ))}
615 <a
616 href={`https://github.com/spboucher-ai/${course.repoName}`}
617 target="_blank"
618 rel="noopener noreferrer"
619 className="inline-flex items-center gap-2 text-sm font-medium text-primary hover:underline"
620 >
621 <Github className="h-4 w-4" aria-hidden="true" />
622 Full course repository
623 </a>
609 + <div>
610 + <PillLink
611 + href={`https://github.com/spboucher-ai/${course.repoName}`}
612 + icon={Github}
613 + >
614 + Full course repository
615 + </PillLink>
616 + </div>
624 617 </CardContent>
625 618 </Card>
626 619 </Reveal>
@@ -631,9 +624,10 @@ export default function TeachingPage() {
631 624 {/* Teaching assistant */}
632 625 <section className="mt-12">
633 626 <Reveal>
634 <h2 className="text-xl font-semibold tracking-tight">
635 Teaching Assistant — Université Laval (2018–2021)
636 </h2>
627 + <SectionHeading
628 + title="Teaching Assistant — Université Laval (2018–2021)"
629 + icon={Users}
630 + />
637 631 </Reveal>
638 632 <div className="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
639 633 {taCourses.map((course, i) => (
modified components/app-card.tsx +2 −2
@@ -22,14 +22,14 @@ import { cn } from "@/lib/utils";
22 22
23 23 export function AppCard({ app }: { app: ZyquoApp }) {
24 24 return (
25 <Card className="flex h-full flex-col hover:-translate-y-1 hover:shadow-[var(--shadow-lift)]">
25 + <Card className="group flex h-full flex-col hover:-translate-y-1 hover:border-primary/25 hover:shadow-[var(--shadow-lift)]">
26 26 <CardHeader className="flex-row items-center gap-4 space-y-0">
27 27 <Image
28 28 src={app.icon}
29 29 alt={`${app.name} app icon`}
30 30 width={64}
31 31 height={64}
32 className="h-16 w-16 shrink-0 rounded-2xl shadow-sm"
32 + className="h-16 w-16 shrink-0 rounded-[1.25rem] shadow-sm ring-1 ring-border/50 transition-transform duration-300 group-hover:scale-105"
33 33 />
34 34 <div className="min-w-0">
35 35 <CardTitle className="text-lg">
added components/icon-tile.tsx +38 −0
@@ -0,0 +1,38 @@
1 +/*
2 + icon-tile.tsx
3 + spboucher.ai Web
4 + Author: Simon-Pierre Boucher
5 + Mail: contact@spboucher.ai
6 +*/
7 +
8 +import type { LucideIcon } from "lucide-react";
9 +
10 +import { cn } from "@/lib/utils";
11 +
12 +const sizes = {
13 + sm: "h-8 w-8 rounded-xl [&_svg]:h-4 [&_svg]:w-4",
14 + md: "h-11 w-11 rounded-2xl [&_svg]:h-5 [&_svg]:w-5",
15 + lg: "h-14 w-14 rounded-3xl [&_svg]:h-6 [&_svg]:w-6",
16 +} as const;
17 +
18 +interface IconTileProps {
19 + icon: LucideIcon;
20 + size?: keyof typeof sizes;
21 + className?: string;
22 +}
23 +
24 +/** A lucide icon set in a soft, rounded gradient tile. */
25 +export function IconTile({ icon: Icon, size = "md", className }: IconTileProps) {
26 + return (
27 + <span
28 + aria-hidden="true"
29 + className={cn(
30 + "inline-flex shrink-0 items-center justify-center bg-gradient-to-br from-primary/15 via-primary/8 to-transparent text-primary ring-1 ring-inset ring-primary/15",
31 + sizes[size],
32 + className,
33 + )}
34 + >
35 + <Icon strokeWidth={1.75} />
36 + </span>
37 + );
38 +}
added components/pill-link.tsx +43 −0
@@ -0,0 +1,43 @@
1 +/*
2 + pill-link.tsx
3 + spboucher.ai Web
4 + Author: Simon-Pierre Boucher
5 + Mail: contact@spboucher.ai
6 +*/
7 +
8 +import type { LucideIcon } from "lucide-react";
9 +
10 +import { cn } from "@/lib/utils";
11 +
12 +interface PillLinkProps {
13 + href: string;
14 + icon?: LucideIcon;
15 + children: React.ReactNode;
16 + external?: boolean;
17 + className?: string;
18 +}
19 +
20 +/** A rounded, chip-style link for documents and resources. */
21 +export function PillLink({
22 + href,
23 + icon: Icon,
24 + children,
25 + external = true,
26 + className,
27 +}: PillLinkProps) {
28 + return (
29 + <a
30 + href={href}
31 + {...(external
32 + ? { target: "_blank", rel: "noopener noreferrer" }
33 + : undefined)}
34 + className={cn(
35 + "inline-flex items-center gap-1.5 rounded-full border border-border/70 bg-card/70 px-3.5 py-1.5 text-xs font-medium text-foreground/80 shadow-sm transition-all hover:-translate-y-px hover:border-primary/40 hover:bg-primary/10 hover:text-primary",
36 + className,
37 + )}
38 + >
39 + {Icon && <Icon className="h-3.5 w-3.5" aria-hidden="true" />}
40 + {children}
41 + </a>
42 + );
43 +}
modified components/project-card.tsx +4 −4
@@ -22,7 +22,7 @@ import { cn } from "@/lib/utils";
22 22
23 23 export function ProjectCard({ project }: { project: OpenSourceProject }) {
24 24 return (
25 <Card className="flex h-full flex-col hover:-translate-y-1 hover:shadow-[var(--shadow-lift)]">
25 + <Card className="group flex h-full flex-col hover:-translate-y-1 hover:border-primary/25 hover:shadow-[var(--shadow-lift)]">
26 26 <CardHeader className="flex-row items-center gap-4 space-y-0">
27 27 {project.icon ? (
28 28 <Image
@@ -30,12 +30,12 @@ export function ProjectCard({ project }: { project: OpenSourceProject }) {
30 30 alt={`${project.name} logo`}
31 31 width={64}
32 32 height={64}
33 className="h-16 w-16 shrink-0 rounded-2xl shadow-sm"
33 + className="h-16 w-16 shrink-0 rounded-[1.25rem] shadow-sm ring-1 ring-border/50 transition-transform duration-300 group-hover:scale-105"
34 34 />
35 35 ) : (
36 36 <div
37 37 aria-hidden="true"
38 className="flex h-16 w-16 shrink-0 items-center justify-center rounded-2xl bg-muted text-3xl shadow-sm"
38 + className="flex h-16 w-16 shrink-0 items-center justify-center rounded-[1.25rem] bg-gradient-to-br from-primary/12 via-primary/6 to-transparent text-3xl shadow-sm ring-1 ring-inset ring-primary/15 transition-transform duration-300 group-hover:scale-105"
39 39 >
40 40 {project.emoji}
41 41 </div>
@@ -78,7 +78,7 @@ export function ProjectCard({ project }: { project: OpenSourceProject }) {
78 78 Download DMG
79 79 </a>
80 80 ) : null}
81 <span className="ml-auto rounded-full border px-2.5 py-0.5 text-xs text-muted-foreground">
81 + <span className="ml-auto rounded-full border border-border/70 bg-muted/60 px-2.5 py-0.5 text-xs font-medium text-muted-foreground">
82 82 {project.language}
83 83 </span>
84 84 </CardFooter>
added components/section-heading.tsx +60 −0
@@ -0,0 +1,60 @@
1 +/*
2 + section-heading.tsx
3 + spboucher.ai Web
4 + Author: Simon-Pierre Boucher
5 + Mail: contact@spboucher.ai
6 +*/
7 +
8 +import type { LucideIcon } from "lucide-react";
9 +
10 +import { IconTile } from "@/components/icon-tile";
11 +import { cn } from "@/lib/utils";
12 +
13 +interface SectionHeadingProps {
14 + eyebrow?: string;
15 + title: string;
16 + description?: string;
17 + icon?: LucideIcon;
18 + as?: "h1" | "h2" | "h3";
19 + className?: string;
20 +}
21 +
22 +/** Unified section header: rounded icon tile, eyebrow, title, description. */
23 +export function SectionHeading({
24 + eyebrow,
25 + title,
26 + description,
27 + icon,
28 + as: Tag = "h2",
29 + className,
30 +}: SectionHeadingProps) {
31 + const isPageTitle = Tag === "h1";
32 + return (
33 + <div className={cn("flex items-start gap-4", className)}>
34 + {icon && <IconTile icon={icon} size={isPageTitle ? "lg" : "md"} />}
35 + <div className="min-w-0">
36 + {eyebrow && (
37 + <p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary">
38 + {eyebrow}
39 + </p>
40 + )}
41 + <Tag
42 + className={cn(
43 + "tracking-tight",
44 + isPageTitle
45 + ? "mt-1 text-3xl font-bold sm:text-4xl"
46 + : "mt-0.5 text-xl font-semibold",
47 + !eyebrow && "mt-0",
48 + )}
49 + >
50 + {title}
51 + </Tag>
52 + {description && (
53 + <p className="mt-2 max-w-3xl text-sm leading-relaxed text-muted-foreground">
54 + {description}
55 + </p>
56 + )}
57 + </div>
58 + </div>
59 + );
60 +}
modified components/site-footer.tsx +61 −23
@@ -6,34 +6,72 @@
6 6 */
7 7
8 8 import Link from "next/link";
9 +import { Github, Mail } from "lucide-react";
9 10
10 11 export function SiteFooter() {
11 12 return (
12 <footer className="border-t border-border/50">
13 <div className="mx-auto flex max-w-5xl flex-col items-center justify-between gap-4 px-4 py-10 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"
13 + <footer className="px-4 pb-6 pt-10 sm:px-6">
14 + <div className="mx-auto max-w-5xl rounded-[1.75rem] border border-border/60 bg-card/60 px-6 py-8 shadow-[var(--shadow-soft)] backdrop-blur-xl sm:px-8">
15 + <div className="flex flex-col items-center justify-between gap-6 sm:flex-row">
16 + <div className="flex items-center gap-3">
17 + <span
18 + aria-hidden="true"
19 + className="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br from-primary to-primary/70 text-xs font-bold tracking-tight text-primary-foreground shadow-sm"
20 + >
21 + SB
22 + </span>
23 + <div>
24 + <p className="text-sm font-semibold tracking-tight">
25 + Simon-Pierre Boucher
26 + </p>
27 + <p className="text-xs text-muted-foreground">
28 + Financial econometrics researcher &amp; macOS/AI developer
29 + </p>
30 + </div>
31 + </div>
32 +
33 + <nav
34 + aria-label="Footer"
35 + className="flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-sm text-muted-foreground"
30 36 >
31 GitHub
32 </a>
33 <Link href="/cv" className="transition-colors hover:text-foreground">
34 CV
35 </Link>
37 + <Link href="/research" className="transition-colors hover:text-primary">
38 + Research
39 + </Link>
40 + <Link href="/teaching" className="transition-colors hover:text-primary">
41 + Teaching
42 + </Link>
43 + <Link href="/apps" className="transition-colors hover:text-primary">
44 + Apps
45 + </Link>
46 + <Link href="/cv" className="transition-colors hover:text-primary">
47 + CV
48 + </Link>
49 + </nav>
50 +
51 + <div className="flex items-center gap-2">
52 + <a
53 + href="mailto:contact@spboucher.ai"
54 + aria-label="Email contact@spboucher.ai"
55 + className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-border/70 bg-card/70 text-muted-foreground transition-all hover:-translate-y-px hover:border-primary/40 hover:bg-primary/10 hover:text-primary"
56 + >
57 + <Mail className="h-4 w-4" aria-hidden="true" />
58 + </a>
59 + <a
60 + href="https://github.com/spboucher-ai"
61 + target="_blank"
62 + rel="noopener noreferrer"
63 + aria-label="GitHub profile"
64 + className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-border/70 bg-card/70 text-muted-foreground transition-all hover:-translate-y-px hover:border-primary/40 hover:bg-primary/10 hover:text-primary"
65 + >
66 + <Github className="h-4 w-4" aria-hidden="true" />
67 + </a>
68 + </div>
36 69 </div>
70 +
71 + <p className="mt-6 border-t border-border/50 pt-5 text-center text-xs text-muted-foreground">
72 + © {new Date().getFullYear()} Simon-Pierre Boucher — Université du
73 + Québec en Outaouais (UQO) · www.spboucher.ai
74 + </p>
37 75 </div>
38 76 </footer>
39 77 );
modified components/site-header.tsx +12 −4
@@ -32,8 +32,16 @@ export function SiteHeader() {
32 32 <header className="sticky top-0 z-50 w-full px-4 pt-3 sm:px-6">
33 33 <div className="mx-auto max-w-5xl rounded-3xl border border-border/60 bg-background/70 shadow-[var(--shadow-soft)] backdrop-blur-xl">
34 34 <div className="flex h-14 items-center justify-between pl-5 pr-3">
35 <Link href="/" className="font-semibold tracking-tight">
36 Simon-Pierre Boucher
35 + <Link href="/" className="flex items-center gap-3">
36 + <span
37 + aria-hidden="true"
38 + className="flex h-8 w-8 items-center justify-center rounded-xl bg-gradient-to-br from-primary to-primary/70 text-[11px] font-bold tracking-tight text-primary-foreground shadow-sm"
39 + >
40 + SB
41 + </span>
42 + <span className="font-semibold tracking-tight">
43 + Simon-Pierre Boucher
44 + </span>
37 45 </Link>
38 46
39 47 <nav className="hidden items-center gap-1 md:flex" aria-label="Main">
@@ -44,7 +52,7 @@ export function SiteHeader() {
44 52 className={cn(
45 53 "rounded-full px-4 py-2 text-sm font-medium transition-colors",
46 54 pathname === item.href
47 ? "bg-secondary text-foreground"
55 + ? "bg-primary/10 text-primary"
48 56 : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
49 57 )}
50 58 >
@@ -81,7 +89,7 @@ export function SiteHeader() {
81 89 className={cn(
82 90 "block rounded-full px-4 py-2 text-sm font-medium transition-colors",
83 91 pathname === item.href
84 ? "bg-secondary text-foreground"
92 + ? "bg-primary/10 text-primary"
85 93 : "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
86 94 )}
87 95 >
modified components/ui/badge.tsx +6 −4
@@ -11,13 +11,15 @@ import { cva, type VariantProps } from "class-variance-authority";
11 11 import { cn } from "@/lib/utils";
12 12
13 13 const badgeVariants = cva(
14 "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors",
14 + "inline-flex items-center gap-1 rounded-full border px-3 py-1 text-xs font-medium transition-colors",
15 15 {
16 16 variants: {
17 17 variant: {
18 default: "border-transparent bg-primary/10 text-primary",
19 secondary: "border-transparent bg-secondary text-secondary-foreground",
20 outline: "border-border text-muted-foreground",
18 + default:
19 + "border-transparent bg-primary/10 text-primary ring-1 ring-inset ring-primary/15",
20 + secondary:
21 + "border-transparent bg-secondary/80 text-secondary-foreground",
22 + outline: "border-border/70 bg-card/50 text-muted-foreground",
21 23 },
22 24 },
23 25 defaultVariants: {
modified components/ui/button.tsx +1 −1
@@ -20,7 +20,7 @@ const buttonVariants = cva(
20 20 secondary:
21 21 "bg-secondary text-secondary-foreground hover:bg-secondary/70",
22 22 outline:
23 "border border-border bg-card/60 hover:border-primary/30 hover:bg-accent hover:text-accent-foreground",
23 + "border border-border/70 bg-card/70 shadow-sm backdrop-blur hover:border-primary/40 hover:bg-primary/10 hover:text-primary",
24 24 ghost: "hover:bg-accent hover:text-accent-foreground",
25 25 link: "text-primary underline-offset-4 hover:underline",
26 26 },
modified components/ui/card.tsx +1 −1
@@ -14,7 +14,7 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
14 14 <div
15 15 ref={ref}
16 16 className={cn(
17 "rounded-3xl border border-border/70 bg-card text-card-foreground shadow-[var(--shadow-soft)] transition-all duration-300",
17 + "rounded-[1.75rem] border border-border/60 bg-card text-card-foreground shadow-[var(--shadow-soft)] transition-all duration-300",
18 18 className
19 19 )}
20 20 {...props}
21 21