|
2 | 2 | import Layout from "@/layouts/Layout.astro"; |
3 | 3 | import { publications } from "@/data/cv"; |
4 | 4 | import { highlightAuthor } from "@/lib/utils"; |
5 | | -import {template} from "@/settings"; |
| 5 | +import { template } from "@/settings"; |
6 | 6 |
|
7 | | -const ALL_PAPERS = publications; // This will be replaced with dynamic data later |
| 7 | +// Group publications by year (descending) |
| 8 | +const grouped = publications.reduce((acc, paper) => { |
| 9 | + const year = paper.time; |
| 10 | + if (!acc[year]) acc[year] = []; |
| 11 | + acc[year].push(paper); |
| 12 | + return acc; |
| 13 | +}, {} as Record<string, typeof publications>); |
| 14 | +
|
| 15 | +const years = Object.keys(grouped).sort((a, b) => Number(b) - Number(a)); |
| 16 | +const totalPapers = publications.length; |
8 | 17 | --- |
9 | 18 |
|
10 | 19 | <Layout title="Papers"> |
11 | | - <div class="prose max-w-none"> |
12 | | - <h1 class="text-3xl font-bold mb-8">Publications</h1> |
13 | | - |
14 | | - <div class="space-y-8"> |
15 | | - { |
16 | | - ALL_PAPERS.map((paper) => ( |
17 | | - <div class="card bg-base-200"> |
18 | | - <div class="card-body"> |
19 | | - <h2 class="card-title text-xl"> |
20 | | - <a href={template.base + paper.link} class="hover:text-accent"> |
21 | | - {paper.title} |
22 | | - </a> |
23 | | - </h2> |
24 | | - <p class="text-base mb-2"> |
25 | | - <span class="font-medium">Authors:</span>{" "} |
26 | | - <span set:html={highlightAuthor(paper.authors)} /> |
27 | | - </p> |
28 | | - <p class="text-sm mb-3"> |
29 | | - <span class="italic">{paper.journal}</span> • {paper.time} |
30 | | - </p> |
31 | | - <p class="text-sm text-base-content/80">{paper.abstract}</p> |
32 | | - <div class="card-actions justify-end mt-4"> |
33 | | - <a href={paper.link} class="btn btn-sm btn-secondary text-secondary-content"> |
34 | | - Read Paper |
35 | | - </a> |
36 | | - </div> |
37 | | - </div> |
38 | | - </div> |
39 | | - )) |
40 | | - } |
| 20 | + <div class="max-w-3xl mx-auto"> |
| 21 | + <!-- Header --> |
| 22 | + <div class="mb-6"> |
| 23 | + <h1 class="text-3xl font-bold">Publications</h1> |
| 24 | + <p class="text-base-content/60 mt-1"> |
| 25 | + {totalPapers} papers · {years[years.length - 1]}–{years[0]} |
| 26 | + </p> |
41 | 27 | </div> |
42 | 28 |
|
43 | | - <div class="flex justify-center mt-8"> |
44 | | - <div class="join"> |
45 | | - <button class="join-item btn btn-sm">«</button> |
46 | | - <button class="join-item btn btn-sm">1</button> |
47 | | - <button class="join-item btn btn-sm btn-active">2</button> |
48 | | - <button class="join-item btn btn-sm">3</button> |
49 | | - <button class="join-item btn btn-sm">»</button> |
50 | | - </div> |
| 29 | + <!-- Controls --> |
| 30 | + <div class="flex justify-end gap-2 mb-4"> |
| 31 | + <button id="expand-all" class="btn btn-xs btn-outline btn-secondary">Expand All</button> |
| 32 | + <button id="collapse-all" class="btn btn-xs btn-outline btn-secondary">Collapse All</button> |
| 33 | + </div> |
| 34 | + |
| 35 | + <!-- Year Sections --> |
| 36 | + <div class="space-y-2"> |
| 37 | + {years.map((year) => ( |
| 38 | + <div class="year-group" data-year={year}> |
| 39 | + <!-- Year Header (clickable) --> |
| 40 | + <button |
| 41 | + class="year-toggle w-full flex items-center gap-3 py-3 cursor-pointer select-none group" |
| 42 | + aria-expanded="true" |
| 43 | + aria-controls={`papers-${year}`} |
| 44 | + > |
| 45 | + <span class="text-2xl font-bold">{year}</span> |
| 46 | + <span class="badge badge-sm badge-secondary">{grouped[year].length}</span> |
| 47 | + <span class="flex-1 h-px bg-base-300"></span> |
| 48 | + <svg |
| 49 | + class="chevron-icon w-5 h-5 text-base-content/50 transition-transform duration-300 group-hover:text-base-content" |
| 50 | + viewBox="0 0 24 24" |
| 51 | + fill="none" |
| 52 | + stroke="currentColor" |
| 53 | + stroke-width="2.2" |
| 54 | + stroke-linecap="round" |
| 55 | + stroke-linejoin="round" |
| 56 | + > |
| 57 | + <polyline points="6 9 12 15 18 9" /> |
| 58 | + </svg> |
| 59 | + </button> |
| 60 | + |
| 61 | + <!-- Papers List --> |
| 62 | + <div id={`papers-${year}`} class="papers-container overflow-hidden transition-all duration-300"> |
| 63 | + <div class="space-y-3 pb-4"> |
| 64 | + {grouped[year].map((paper) => ( |
| 65 | + <div class="card bg-base-200 shadow-sm hover:shadow-md transition-shadow duration-200"> |
| 66 | + <div class="card-body p-5"> |
| 67 | + <!-- Title --> |
| 68 | + <h2 class="card-title text-base leading-snug"> |
| 69 | + <a |
| 70 | + href={paper.link} |
| 71 | + class="hover:text-accent transition-colors duration-200" |
| 72 | + target="_blank" |
| 73 | + rel="noopener noreferrer" |
| 74 | + > |
| 75 | + {paper.title} |
| 76 | + </a> |
| 77 | + </h2> |
| 78 | + |
| 79 | + <!-- Authors --> |
| 80 | + <p class="text-sm text-base-content/70 mt-1"> |
| 81 | + <span set:html={highlightAuthor(paper.authors)} /> |
| 82 | + </p> |
| 83 | + |
| 84 | + <!-- Journal + Link row --> |
| 85 | + <div class="flex items-center justify-between flex-wrap gap-2 mt-2"> |
| 86 | + <span class="text-sm italic text-base-content/60">{paper.journal}</span> |
| 87 | + <a |
| 88 | + href={paper.link} |
| 89 | + target="_blank" |
| 90 | + rel="noopener noreferrer" |
| 91 | + class="btn btn-xs btn-outline btn-accent gap-1" |
| 92 | + > |
| 93 | + DOI |
| 94 | + <svg class="w-3 h-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> |
| 95 | + <path d="M7 17L17 7" /><path d="M7 7h10v10" /> |
| 96 | + </svg> |
| 97 | + </a> |
| 98 | + </div> |
| 99 | + |
| 100 | + <!-- Abstract Toggle --> |
| 101 | + {paper.abstract && ( |
| 102 | + <div class="mt-2"> |
| 103 | + <button class="abstract-toggle text-xs font-medium text-base-content/50 hover:text-accent transition-colors cursor-pointer"> |
| 104 | + ▸ Abstract |
| 105 | + </button> |
| 106 | + <div class="abstract-text hidden mt-2 text-sm text-base-content/70 leading-relaxed p-3 bg-base-300/40 rounded-lg border-l-3 border-accent/30"> |
| 107 | + {paper.abstract} |
| 108 | + </div> |
| 109 | + </div> |
| 110 | + )} |
| 111 | + </div> |
| 112 | + </div> |
| 113 | + ))} |
| 114 | + </div> |
| 115 | + </div> |
| 116 | + </div> |
| 117 | + ))} |
51 | 118 | </div> |
52 | 119 | </div> |
53 | 120 | </Layout> |
| 121 | + |
| 122 | +<script> |
| 123 | + document.addEventListener("astro:page-load", () => { |
| 124 | + // Year section collapse/expand |
| 125 | + const yearToggles = document.querySelectorAll<HTMLButtonElement>(".year-toggle"); |
| 126 | + |
| 127 | + yearToggles.forEach((toggle) => { |
| 128 | + const container = toggle.nextElementSibling as HTMLElement; |
| 129 | + if (!container) return; |
| 130 | + |
| 131 | + // Set initial max-height for smooth animation |
| 132 | + container.style.maxHeight = container.scrollHeight + "px"; |
| 133 | + |
| 134 | + toggle.addEventListener("click", () => { |
| 135 | + const expanded = toggle.getAttribute("aria-expanded") === "true"; |
| 136 | + toggle.setAttribute("aria-expanded", String(!expanded)); |
| 137 | + |
| 138 | + const chevron = toggle.querySelector(".chevron-icon") as HTMLElement; |
| 139 | + |
| 140 | + if (expanded) { |
| 141 | + // Collapse |
| 142 | + container.style.maxHeight = "0px"; |
| 143 | + container.style.opacity = "0"; |
| 144 | + if (chevron) chevron.style.transform = "rotate(-90deg)"; |
| 145 | + } else { |
| 146 | + // Expand |
| 147 | + container.style.maxHeight = container.scrollHeight + "px"; |
| 148 | + container.style.opacity = "1"; |
| 149 | + if (chevron) chevron.style.transform = "rotate(0deg)"; |
| 150 | + } |
| 151 | + }); |
| 152 | + }); |
| 153 | + |
| 154 | + // Expand All |
| 155 | + document.getElementById("expand-all")?.addEventListener("click", () => { |
| 156 | + yearToggles.forEach((toggle) => { |
| 157 | + toggle.setAttribute("aria-expanded", "true"); |
| 158 | + const container = toggle.nextElementSibling as HTMLElement; |
| 159 | + const chevron = toggle.querySelector(".chevron-icon") as HTMLElement; |
| 160 | + if (container) { |
| 161 | + container.style.maxHeight = container.scrollHeight + "px"; |
| 162 | + container.style.opacity = "1"; |
| 163 | + } |
| 164 | + if (chevron) chevron.style.transform = "rotate(0deg)"; |
| 165 | + }); |
| 166 | + }); |
| 167 | + |
| 168 | + // Collapse All |
| 169 | + document.getElementById("collapse-all")?.addEventListener("click", () => { |
| 170 | + yearToggles.forEach((toggle) => { |
| 171 | + toggle.setAttribute("aria-expanded", "false"); |
| 172 | + const container = toggle.nextElementSibling as HTMLElement; |
| 173 | + const chevron = toggle.querySelector(".chevron-icon") as HTMLElement; |
| 174 | + if (container) { |
| 175 | + container.style.maxHeight = "0px"; |
| 176 | + container.style.opacity = "0"; |
| 177 | + } |
| 178 | + if (chevron) chevron.style.transform = "rotate(-90deg)"; |
| 179 | + }); |
| 180 | + }); |
| 181 | + |
| 182 | + // Abstract toggles |
| 183 | + document.querySelectorAll<HTMLButtonElement>(".abstract-toggle").forEach((btn) => { |
| 184 | + btn.addEventListener("click", () => { |
| 185 | + const abstractText = btn.nextElementSibling as HTMLElement; |
| 186 | + if (!abstractText) return; |
| 187 | + |
| 188 | + const isHidden = abstractText.classList.contains("hidden"); |
| 189 | + abstractText.classList.toggle("hidden"); |
| 190 | + btn.textContent = isHidden ? "▾ Abstract" : "▸ Abstract"; |
| 191 | + }); |
| 192 | + }); |
| 193 | + }); |
| 194 | +</script> |
| 195 | + |
| 196 | +<style> |
| 197 | + .papers-container { |
| 198 | + transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1), |
| 199 | + opacity 0.25s ease; |
| 200 | + } |
| 201 | +</style> |
0 commit comments