SPB Git

Repository Public

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>
Simon-Pierre Boucher committed · Nov 1, 2026 parent 7983533
9 changed files +273 −2 Browse files
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 &amp; 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.

Menu

Theme