Commit
Book: add the Quebec real-estate appraisal treatise (PDF + cover) to Research, Home and Teaching
- lib/books.ts: treatise metadata (1,933 p., 48 ch., 251 fig., 432 tables, 338 exercises, v0.1 Nov. 2026) - components/book-card.tsx: cover + stats + Read PDF / Sources buttons (full and compact variants) - /research: Book section + stats strip; home: highlights card + stat; /teaching: Textbook section - public/books: independent edition PDF (no institutional mark) and cover; agent profile knows the book Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
modified
CLAUDE.md
+1 −0
@@ -74,6 +74,7 @@ The public route `https://www.spboucher.ai → M2U64:3100` lives on the MacLustr | ||
| 74 | 74 | - `lib/research-details.ts` — one rich entry per paper for `/research/[slug]` (hero, abstract, findings, key results, data, methodology, reproducibility, keywords, JEL). |
| 75 | 75 | - PDFs are served from `public/papers/<slug>_uqo.pdf`; source of truth for the papers is `~/Desktop/UQO/Recherche/Recherche-UQO` on the laptop (one folder per paper, `paper/main.pdf`). |
| 76 | 76 | - To add a paper: copy the PDF, append an entry to both files (same `slug`), rebuild. |
| 77 | +- `lib/books.ts` + `components/book-card.tsx` — books (currently the Quebec appraisal treatise, PDF `public/books/traite-evaluation-qc.pdf` = the *independent* edition without institutional mark, cover `public/books/*-cover.jpg`). Shown on /research (#book), the home highlights and /teaching (Textbook). | |
| 77 | 78 | |
| 78 | 79 | ## Site Structure |
| 79 | 80 | |
modified
app/page.tsx
+10 −1
@@ -10,6 +10,7 @@ import Link from "next/link"; | ||
| 10 | 10 | import { |
| 11 | 11 | AppWindow, |
| 12 | 12 | ArrowRight, |
| 13 | + BookMarked, | |
| 13 | 14 | BookOpenCheck, |
| 14 | 15 | Github, |
| 15 | 16 | GraduationCap, |
@@ -35,12 +36,15 @@ import { Reveal } from "@/components/reveal"; | ||
| 35 | 36 | import { SectionHeading } from "@/components/section-heading"; |
| 36 | 37 | import { cn } from "@/lib/utils"; |
| 37 | 38 | import { getFieldInfo, researchStats, uqoWorkingPapers } from "@/lib/research"; |
| 39 | +import { books } from "@/lib/books"; | |
| 40 | +import { BookCard } from "@/components/book-card"; | |
| 38 | 41 | |
| 39 | 42 | const latestPapers = uqoWorkingPapers.filter((p) => p.kind === "wp").slice(0, 4); |
| 40 | 43 | |
| 41 | 44 | const stats = [ |
| 42 | 45 | { icon: BookOpenCheck, value: "1", label: "Peer-reviewed publication" }, |
| 43 | 46 | { icon: ScrollText, value: String(researchStats.papers), label: "Working papers" }, |
| 47 | + { icon: BookMarked, value: String(books.length), label: "Book (1,933 pages)" }, | |
| 44 | 48 | { icon: AppWindow, value: "9", label: "Native macOS apps" }, |
| 45 | 49 | { icon: Github, value: "30+", label: "Open-source repositories" }, |
| 46 | 50 | ]; |
@@ -312,7 +316,7 @@ export default function HomePage() { | ||
| 312 | 316 | eyebrow="Latest" |
| 313 | 317 | title="Research Highlights" |
| 314 | 318 | icon={BookOpenCheck} |
| 315 | − description={`A peer-reviewed publication in The Energy Journal and a series of ${researchStats.papers} UQO working papers — ${researchStats.pages.toLocaleString("en-US")} pages of research, each with a public code repository.`} | |
| 319 | + description={`A peer-reviewed publication in The Energy Journal, a ${books[0].pages.toLocaleString("en-US")}-page treatise on real-estate appraisal in Quebec, and a series of ${researchStats.papers} UQO working papers — each with a public code repository.`} | |
| 316 | 320 | /> |
| 317 | 321 | </Reveal> |
| 318 | 322 | <div className="mt-6 grid gap-4 lg:grid-cols-[1.1fr_1fr]"> |
@@ -388,6 +392,11 @@ export default function HomePage() { | ||
| 388 | 392 | </Card> |
| 389 | 393 | </Reveal> |
| 390 | 394 | </div> |
| 395 | + <div className="mt-4"> | |
| 396 | + <Reveal delay={0.12}> | |
| 397 | + <BookCard book={books[0]} variant="compact" /> | |
| 398 | + </Reveal> | |
| 399 | + </div> | |
| 391 | 400 | </section> |
| 392 | 401 | |
| 393 | 402 | {/* Skills */} |
modified
app/research/page.tsx
+23 −1
@@ -10,6 +10,7 @@ import Link from "next/link"; | ||
| 10 | 10 | import { |
| 11 | 11 | ArrowUpRight, |
| 12 | 12 | BookOpenCheck, |
| 13 | + BookMarked, | |
| 13 | 14 | ExternalLink, |
| 14 | 15 | FileText, |
| 15 | 16 | FlaskConical, |
@@ -20,6 +21,7 @@ import { | ||
| 20 | 21 | } from "lucide-react"; |
| 21 | 22 | |
| 22 | 23 | import { Badge } from "@/components/ui/badge"; |
| 24 | +import { BookCard } from "@/components/book-card"; | |
| 23 | 25 | import { |
| 24 | 26 | Card, |
| 25 | 27 | CardContent, |
@@ -37,6 +39,7 @@ import { | ||
| 37 | 39 | thesisChapters, |
| 38 | 40 | uqoWorkingPapers, |
| 39 | 41 | } from "@/lib/research"; |
| 42 | +import { books } from "@/lib/books"; | |
| 40 | 43 | |
| 41 | 44 | export const metadata: Metadata = { |
| 42 | 45 | title: "Research", |
@@ -80,6 +83,7 @@ const headerStats = [ | ||
| 80 | 83 | }, |
| 81 | 84 | { value: String(researchStats.onSsrn), label: "posted on SSRN" }, |
| 82 | 85 | { value: String(researchStats.fields), label: "research fields" }, |
| 86 | + { value: String(books.length), label: "book" }, | |
| 83 | 87 | ]; |
| 84 | 88 | |
| 85 | 89 | export default function ResearchPage() { |
@@ -96,7 +100,7 @@ export default function ResearchPage() { | ||
| 96 | 100 | </Reveal> |
| 97 | 101 | |
| 98 | 102 | <Reveal delay={0.08}> |
| 99 | − <div className="mt-10 grid grid-cols-2 gap-x-6 gap-y-6 border-t-2 border-foreground pt-5 sm:grid-cols-4"> | |
| 103 | + <div className="mt-10 grid grid-cols-2 gap-x-6 gap-y-6 border-t-2 border-foreground pt-5 sm:grid-cols-5"> | |
| 100 | 104 | {headerStats.map((s, i) => ( |
| 101 | 105 | <div key={s.label}> |
| 102 | 106 | <p className="font-mono text-[0.66rem] uppercase tracking-[0.16em] text-muted-foreground/70"> |
@@ -141,6 +145,24 @@ export default function ResearchPage() { | ||
| 141 | 145 | </Reveal> |
| 142 | 146 | </section> |
| 143 | 147 | |
| 148 | + {/* Book */} | |
| 149 | + <section className="mt-14" id="book"> | |
| 150 | + <Reveal> | |
| 151 | + <SectionHeading | |
| 152 | + title="Book" | |
| 153 | + icon={BookMarked} | |
| 154 | + description="A reproducible university treatise on real-estate appraisal in Quebec — the full text is free to read, and every figure and number is regenerated by the scripts in its companion repository." | |
| 155 | + /> | |
| 156 | + </Reveal> | |
| 157 | + <div className="mt-4 space-y-4"> | |
| 158 | + {books.map((book, i) => ( | |
| 159 | + <Reveal key={book.slug} delay={i * 0.08}> | |
| 160 | + <BookCard book={book} /> | |
| 161 | + </Reveal> | |
| 162 | + ))} | |
| 163 | + </div> | |
| 164 | + </section> | |
| 165 | + | |
| 144 | 166 | {/* UQO working paper series */} |
| 145 | 167 | <section className="mt-14" id="working-papers"> |
| 146 | 168 | <Reveal> |
modified
app/teaching/page.tsx
+19 −0
@@ -9,6 +9,7 @@ import type { Metadata } from "next"; | ||
| 9 | 9 | import type { LucideIcon } from "lucide-react"; |
| 10 | 10 | import { |
| 11 | 11 | BadgeCheck, |
| 12 | + BookMarked, | |
| 12 | 13 | BookOpen, |
| 13 | 14 | FileText, |
| 14 | 15 | Github, |
@@ -34,6 +35,8 @@ import { | ||
| 34 | 35 | import { PillLink } from "@/components/pill-link"; |
| 35 | 36 | import { Reveal } from "@/components/reveal"; |
| 36 | 37 | import { SectionHeading } from "@/components/section-heading"; |
| 38 | +import { BookCard } from "@/components/book-card"; | |
| 39 | +import { books } from "@/lib/books"; | |
| 37 | 40 | |
| 38 | 41 | export const metadata: Metadata = { |
| 39 | 42 | title: "Teaching", |
@@ -386,6 +389,22 @@ export default function TeachingPage() { | ||
| 386 | 389 | /> |
| 387 | 390 | </Reveal> |
| 388 | 391 | |
| 392 | + {/* Textbook */} | |
| 393 | + <section className="mt-12"> | |
| 394 | + <Reveal> | |
| 395 | + <SectionHeading | |
| 396 | + title="Textbook" | |
| 397 | + icon={BookMarked} | |
| 398 | + description="The IMM1003 and IMM1033 courses draw on this treatise, which covers the whole appraisal curriculum — legal framework, market analysis, the three approaches, depreciation, hedonic modelling, assessment rolls and complete worked cases." | |
| 399 | + /> | |
| 400 | + </Reveal> | |
| 401 | + <div className="mt-4"> | |
| 402 | + <Reveal delay={0.06}> | |
| 403 | + <BookCard book={books[0]} variant="compact" /> | |
| 404 | + </Reveal> | |
| 405 | + </div> | |
| 406 | + </section> | |
| 407 | + | |
| 389 | 408 | {/* UQO courses with material */} |
| 390 | 409 | <section className="mt-12"> |
| 391 | 410 | <Reveal> |
added
components/book-card.tsx
+135 −0
@@ -0,0 +1,135 @@ | ||
| 1 | +/* | |
| 2 | + book-card.tsx | |
| 3 | + spboucher.ai Web | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +*/ | |
| 7 | + | |
| 8 | +import Image from "next/image"; | |
| 9 | +import { FileText, Github } from "lucide-react"; | |
| 10 | + | |
| 11 | +import { Badge } from "@/components/ui/badge"; | |
| 12 | +import { buttonVariants } from "@/components/ui/button"; | |
| 13 | +import type { Book } from "@/lib/books"; | |
| 14 | +import { cn } from "@/lib/utils"; | |
| 15 | + | |
| 16 | +interface BookCardProps { | |
| 17 | + book: Book; | |
| 18 | + /** "full" = cover + stats + notice (research page); "compact" = cover + title + buttons. */ | |
| 19 | + variant?: "full" | "compact"; | |
| 20 | + className?: string; | |
| 21 | +} | |
| 22 | + | |
| 23 | +/** A book presented as a cover with its key figures and download links. */ | |
| 24 | +export function BookCard({ book, variant = "full", className }: BookCardProps) { | |
| 25 | + const stats = [ | |
| 26 | + { value: book.pages.toLocaleString("en-US"), label: "pages" }, | |
| 27 | + { value: String(book.chapters), label: "chapters" }, | |
| 28 | + { value: String(book.figures), label: "figures" }, | |
| 29 | + { value: String(book.tables), label: "tables" }, | |
| 30 | + { value: String(book.exercises), label: "solved exercises" }, | |
| 31 | + ]; | |
| 32 | + const compact = variant === "compact"; | |
| 33 | + | |
| 34 | + return ( | |
| 35 | + <article | |
| 36 | + className={cn( | |
| 37 | + "grid gap-6 rounded-[0.9rem] border border-border bg-card p-5 shadow-[var(--shadow-soft)] transition-all duration-300 hover:shadow-[var(--shadow-lift)] sm:p-6", | |
| 38 | + compact ? "sm:grid-cols-[7rem_1fr]" : "md:grid-cols-[11rem_1fr]", | |
| 39 | + className, | |
| 40 | + )} | |
| 41 | + > | |
| 42 | + <a | |
| 43 | + href={book.pdf} | |
| 44 | + target="_blank" | |
| 45 | + rel="noopener noreferrer" | |
| 46 | + className="group block w-28 shrink-0 sm:w-auto" | |
| 47 | + aria-label={`${book.title} — PDF`} | |
| 48 | + > | |
| 49 | + <Image | |
| 50 | + src={book.cover} | |
| 51 | + alt={`Cover of ${book.title}`} | |
| 52 | + width={1120} | |
| 53 | + height={1600} | |
| 54 | + sizes={compact ? "112px" : "(min-width: 768px) 176px, 112px"} | |
| 55 | + className="w-full border border-border/70 shadow-[var(--shadow-lift)] transition-transform duration-300 group-hover:-translate-y-1" | |
| 56 | + /> | |
| 57 | + </a> | |
| 58 | + | |
| 59 | + <div className="min-w-0"> | |
| 60 | + <div className="flex flex-wrap items-center gap-1.5"> | |
| 61 | + <Badge>Book · {book.year}</Badge> | |
| 62 | + <Badge variant="outline">{book.edition}</Badge> | |
| 63 | + <Badge variant="outline">{book.language}</Badge> | |
| 64 | + </div> | |
| 65 | + <h3 | |
| 66 | + className={cn( | |
| 67 | + "mt-3 font-display font-semibold leading-snug tracking-tight", | |
| 68 | + compact ? "text-lg" : "text-xl sm:text-2xl", | |
| 69 | + )} | |
| 70 | + > | |
| 71 | + {book.title} | |
| 72 | + </h3> | |
| 73 | + <p className="mt-1 font-display text-base leading-snug text-foreground/75 [font-style:italic]"> | |
| 74 | + {book.subtitle} | |
| 75 | + </p> | |
| 76 | + {!compact && ( | |
| 77 | + <p className="mt-2 text-xs text-muted-foreground"> | |
| 78 | + {book.titleEn} — {book.authors.join(", ")}, Université du Québec en | |
| 79 | + Outaouais, {book.date}. | |
| 80 | + </p> | |
| 81 | + )} | |
| 82 | + <p | |
| 83 | + className={cn( | |
| 84 | + "mt-3 text-sm leading-relaxed text-muted-foreground", | |
| 85 | + compact && "line-clamp-3", | |
| 86 | + )} | |
| 87 | + > | |
| 88 | + {book.description} | |
| 89 | + </p> | |
| 90 | + | |
| 91 | + {!compact && ( | |
| 92 | + <div className="mt-5 grid grid-cols-3 gap-x-4 gap-y-4 border-t border-border/70 pt-4 sm:grid-cols-5"> | |
| 93 | + {stats.map((s) => ( | |
| 94 | + <div key={s.label}> | |
| 95 | + <p className="font-display text-xl font-semibold tracking-tight sm:text-2xl"> | |
| 96 | + {s.value} | |
| 97 | + </p> | |
| 98 | + <p className="mt-0.5 font-mono text-[0.62rem] uppercase tracking-[0.12em] text-muted-foreground"> | |
| 99 | + {s.label} | |
| 100 | + </p> | |
| 101 | + </div> | |
| 102 | + ))} | |
| 103 | + </div> | |
| 104 | + )} | |
| 105 | + | |
| 106 | + <div className="mt-5 flex flex-wrap gap-2"> | |
| 107 | + <a | |
| 108 | + href={book.pdf} | |
| 109 | + target="_blank" | |
| 110 | + rel="noopener noreferrer" | |
| 111 | + className={buttonVariants({ size: "sm" })} | |
| 112 | + > | |
| 113 | + <FileText aria-hidden="true" /> | |
| 114 | + Read the book (PDF, {book.pdfMb} MB) | |
| 115 | + </a> | |
| 116 | + <a | |
| 117 | + href={book.repo} | |
| 118 | + target="_blank" | |
| 119 | + rel="noopener noreferrer" | |
| 120 | + className={buttonVariants({ variant: "outline", size: "sm" })} | |
| 121 | + > | |
| 122 | + <Github aria-hidden="true" /> | |
| 123 | + Sources, data & scripts | |
| 124 | + </a> | |
| 125 | + </div> | |
| 126 | + | |
| 127 | + {!compact && ( | |
| 128 | + <p className="mt-4 text-xs leading-relaxed text-muted-foreground/80"> | |
| 129 | + {book.notice} | |
| 130 | + </p> | |
| 131 | + )} | |
| 132 | + </div> | |
| 133 | + </article> | |
| 134 | + ); | |
| 135 | +} | |
modified
lib/agent-profile.ts
+5 −0
@@ -7,6 +7,7 @@ | ||
| 7 | 7 | |
| 8 | 8 | import { iosApps, openSourceProjects, zyquoApps } from "./apps"; |
| 9 | 9 | import { allResearchPapers } from "./research"; |
| 10 | +import { books } from "./books"; | |
| 10 | 11 | |
| 11 | 12 | /** |
| 12 | 13 | * System prompt for the SPB Agent chat widget. Built from the same data |
@@ -55,6 +56,10 @@ export function buildAgentSystemPrompt(): string { | ||
| 55 | 56 | - Bilingual: French and English. |
| 56 | 57 | - Contact: academic simon-pierre.boucher@uqo.ca · personal contact@spboucher.ai · code at https://git.spboucher.ai. |
| 57 | 58 | |
| 59 | +# Book | |
| 60 | + | |
| 61 | +${books.map((b) => `- "${b.title} — ${b.subtitle}" (${b.language}, ${b.edition}, ${b.date}, ${b.pages} pages, ${b.chapters} chapters): ${b.description} Free PDF: https://www.spboucher.ai${b.pdf} · Sources: ${b.repo}`).join("\n")} | |
| 62 | + | |
| 58 | 63 | # Research papers |
| 59 | 64 | |
| 60 | 65 | ${paperLines} |
added
lib/books.ts
+80 −0
@@ -0,0 +1,80 @@ | ||
| 1 | +/* | |
| 2 | + books.ts | |
| 3 | + spboucher.ai Web | |
| 4 | + Author: Simon-Pierre Boucher | |
| 5 | + Mail: contact@spboucher.ai | |
| 6 | +*/ | |
| 7 | + | |
| 8 | +export interface Book { | |
| 9 | + slug: string; | |
| 10 | + /** Original title (French). */ | |
| 11 | + title: string; | |
| 12 | + subtitle: string; | |
| 13 | + /** English rendering of the title for context. */ | |
| 14 | + titleEn: string; | |
| 15 | + language: string; | |
| 16 | + edition: string; | |
| 17 | + date: string; | |
| 18 | + year: number; | |
| 19 | + pages: number; | |
| 20 | + chapters: number; | |
| 21 | + appendices: number; | |
| 22 | + figures: number; | |
| 23 | + tables: number; | |
| 24 | + equations: number; | |
| 25 | + exercises: number; | |
| 26 | + /** Size of the PDF, in megabytes (shown on the download button). */ | |
| 27 | + pdfMb: number; | |
| 28 | + authors: string[]; | |
| 29 | + description: string; | |
| 30 | + notice: string; | |
| 31 | + cover: string; | |
| 32 | + pdf: string; | |
| 33 | + repo: string; | |
| 34 | + keywords: string[]; | |
| 35 | +} | |
| 36 | + | |
| 37 | +export const books: Book[] = [ | |
| 38 | + { | |
| 39 | + slug: "traite-evaluation-qc", | |
| 40 | + title: | |
| 41 | + "Évaluation immobilière résidentielle et multirésidentielle au Québec", | |
| 42 | + subtitle: "Fondements, méthodes et applications avancées", | |
| 43 | + titleEn: | |
| 44 | + "Residential and Multi-Residential Real Estate Appraisal in Quebec: Foundations, Methods and Advanced Applications", | |
| 45 | + language: "French", | |
| 46 | + edition: "Working version 0.1", | |
| 47 | + date: "November 2026", | |
| 48 | + year: 2026, | |
| 49 | + pages: 1933, | |
| 50 | + chapters: 48, | |
| 51 | + appendices: 6, | |
| 52 | + figures: 251, | |
| 53 | + tables: 432, | |
| 54 | + equations: 682, | |
| 55 | + exercises: 338, | |
| 56 | + pdfMb: 13, | |
| 57 | + authors: ["Simon-Pierre Boucher"], | |
| 58 | + description: | |
| 59 | + "A university treatise on residential and multi-residential appraisal in Quebec: the legal and institutional framework, market analysis, the sales-comparison, cost and income approaches, land valuation, depreciation, hedonic and panel models, assessment rolls and taxation, and four complete worked cases. Every number, figure and table is regenerated by scripts in the companion repository.", | |
| 60 | + notice: | |
| 61 | + "Working version, not approved, published or endorsed by UQO; data marked synthetic describe no real property; legal rules are cited with their consultation date.", | |
| 62 | + cover: "/books/traite-evaluation-qc-cover.jpg", | |
| 63 | + pdf: "/books/traite-evaluation-qc.pdf", | |
| 64 | + repo: "https://git.spboucher.ai/traite-evaluation-qc", | |
| 65 | + keywords: [ | |
| 66 | + "real estate appraisal", | |
| 67 | + "Quebec", | |
| 68 | + "sales comparison", | |
| 69 | + "cost approach", | |
| 70 | + "income approach", | |
| 71 | + "hedonic models", | |
| 72 | + "property assessment", | |
| 73 | + "textbook", | |
| 74 | + ], | |
| 75 | + }, | |
| 76 | +]; | |
| 77 | + | |
| 78 | +export function getBook(slug: string): Book | undefined { | |
| 79 | + return books.find((b) => b.slug === slug); | |
| 80 | +} | |
added
public/books/traite-evaluation-qc-cover.jpg
+0 −0
Binary file not shown.
added
public/books/traite-evaluation-qc.pdf
+0 −0
Binary file not shown.