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>
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 & 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 & 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 & 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 & 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 | |