spb/spboucher.ai
Public
spboucher.ai — personal website of Simon-Pierre Boucher.
1/*2 book-card.tsx3 spboucher.ai Web4 Author: Simon-Pierre Boucher5 Mail: contact@spboucher.ai6*/78import Image from "next/image";9import { FileText, Github } from "lucide-react";1011import { Badge } from "@/components/ui/badge";12import { buttonVariants } from "@/components/ui/button";13import type { Book } from "@/lib/books";14import { cn } from "@/lib/utils";1516interface BookCardProps {17 book: Book;18 /** "full" = cover + stats + notice (research page); "compact" = cover + title + buttons. */19 variant?: "full" | "compact";20 className?: string;21}2223/** A book presented as a cover with its key figures and download links. */24export 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";3334 return (35 <article36 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 <a43 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 <Image50 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>5859 <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 <h366 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 en79 Outaouais, {book.date}.80 </p>81 )}82 <p83 className={cn(84 "mt-3 text-sm leading-relaxed text-muted-foreground",85 compact && "line-clamp-3",86 )}87 >88 {book.description}89 </p>9091 {!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 )}105106 <div className="mt-5 flex flex-wrap gap-2">107 <a108 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 <a117 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 & scripts124 </a>125 </div>126127 {!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}136