Skip to content

Commit 22bcee4

Browse files
authored
fix: scrolling gap; add: Pauline, research tab, epfl footer, student google form (#3)
1 parent 70eea73 commit 22bcee4

File tree

13 files changed

+533
-179
lines changed

13 files changed

+533
-179
lines changed

app/people/page.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default function PeoplePage() {
1717
const pi = currentMembers.find(p => p.role === "Principal Investigator")
1818
const phd = currentMembers.filter(p => p.role === "PhD Student")
1919
const staff = currentMembers.filter(p => p.role === "Research Staff" || p.role === "Software Engineer")
20+
const admin = currentMembers.filter(p => p.role === "Lab Administration")
2021

2122
return (
2223
<div className="flex flex-col">
@@ -230,6 +231,69 @@ export default function PeoplePage() {
230231
</section>
231232
)}
232233

234+
{/* Lab Administration */}
235+
{admin.length > 0 && (
236+
<section className="bg-muted/30 py-16 lg:py-24">
237+
<div className="section-container">
238+
<h2 className="mb-8 text-2xl font-bold">Lab Administration</h2>
239+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
240+
{admin.map((person) => (
241+
<Card key={person.id} className="group transition-all hover:shadow-soft-lg">
242+
<CardHeader>
243+
<div className="mb-3 flex items-start gap-4">
244+
{person.avatar && (
245+
<div className="relative size-28 shrink-0 overflow-hidden rounded-xl ring-2 ring-border">
246+
<img
247+
src={person.avatar}
248+
alt={person.name}
249+
className="size-full object-cover"
250+
/>
251+
</div>
252+
)}
253+
<div className="min-w-0 flex-1">
254+
<CardTitle className="text-lg leading-tight">{person.name}</CardTitle>
255+
<p className="mt-1 text-sm text-muted-foreground">{person.role}</p>
256+
</div>
257+
</div>
258+
{person.project && (
259+
<CardDescription className="mt-2">{person.project}</CardDescription>
260+
)}
261+
{person.note && (
262+
<p className="mt-2 text-sm italic text-muted-foreground">{person.note}</p>
263+
)}
264+
</CardHeader>
265+
<CardContent>
266+
{person.links && (
267+
<div className="mb-3 flex flex-wrap gap-3">
268+
{Object.entries(person.links).map(([key, url]) => (
269+
<a
270+
key={key}
271+
href={url as string}
272+
target="_blank"
273+
rel="noopener noreferrer"
274+
className="inline-flex items-center gap-1 text-xs text-primary hover:underline"
275+
>
276+
<ExternalLink className="size-3" />
277+
{key.charAt(0).toUpperCase() + key.slice(1)}
278+
</a>
279+
))}
280+
</div>
281+
)}
282+
{person.tags && person.tags.length > 0 && (
283+
<div className="flex flex-wrap gap-2">
284+
{person.tags.map((tag) => (
285+
<Badge key={tag} variant="secondary" className="text-xs">{tag}</Badge>
286+
))}
287+
</div>
288+
)}
289+
</CardContent>
290+
</Card>
291+
))}
292+
</div>
293+
</div>
294+
</section>
295+
)}
296+
233297
{/* Alumni */}
234298
{alumni.length > 0 && (
235299
<section className="bg-muted/30 py-16 lg:py-24">

app/positions/page.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,14 @@ export default function PositionsPage() {
6161
</div>
6262
<div className="mt-6">
6363
<Button asChild>
64-
<Link href="/contact">
65-
Get in Touch
66-
<ArrowRight className="ml-2 size-4" />
67-
</Link>
64+
<a
65+
href="https://docs.google.com/forms/d/e/1FAIpQLScyKbu2Hfv24i3RClKZsAEnt8Rzo77RQ27w-VIo4fZEFk8QFg/viewform"
66+
target="_blank"
67+
rel="noopener noreferrer"
68+
>
69+
Express Interest (EPFL Students)
70+
<ExternalLink className="ml-2 size-4" />
71+
</a>
6872
</Button>
6973
</div>
7074
</CardContent>

app/publications/page.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,23 @@ import { Badge } from "@/components/ui/badge"
77
import { Button } from "@/components/ui/button"
88
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
99
import publicationsData from "@/data/publications.json"
10+
import { useHeaderOffset } from "@/lib/hooks/useHeaderOffset"
1011

1112
export default function PublicationsPage() {
1213
const [selectedYear, setSelectedYear] = useState<number | null>(null)
1314
const [selectedType, setSelectedType] = useState<string | null>(null)
1415
const filterRef = useRef<HTMLElement | null>(null)
1516
const [yearStickyOffset, setYearStickyOffset] = useState<number | null>(null)
17+
const headerOffset = useHeaderOffset()
1618

1719
useEffect(() => {
20+
if (headerOffset === null) {
21+
return
22+
}
23+
1824
const updateOffset = () => {
19-
const header = document.querySelector<HTMLElement>("header")
20-
const headerHeight = header?.getBoundingClientRect().height ?? 0
2125
const filterHeight = filterRef.current?.getBoundingClientRect().height ?? 0
22-
const combinedHeight = headerHeight + filterHeight
26+
const combinedHeight = headerOffset + filterHeight
2327

2428
setYearStickyOffset(prev => {
2529
if (prev !== null && Math.abs(prev - combinedHeight) < 0.5) {
@@ -35,10 +39,6 @@ export default function PublicationsPage() {
3539
let resizeObserver: ResizeObserver | null = null
3640
if (typeof ResizeObserver !== "undefined") {
3741
resizeObserver = new ResizeObserver(updateOffset)
38-
const header = document.querySelector<HTMLElement>("header")
39-
if (header) {
40-
resizeObserver.observe(header)
41-
}
4242
if (filterRef.current) {
4343
resizeObserver.observe(filterRef.current)
4444
}
@@ -48,7 +48,7 @@ export default function PublicationsPage() {
4848
window.removeEventListener("resize", updateOffset)
4949
resizeObserver?.disconnect()
5050
}
51-
}, [])
51+
}, [headerOffset])
5252

5353
const years = Array.from(new Set(publicationsData.map(p => p.year))).sort((a, b) => b - a)
5454
const types = Array.from(new Set(publicationsData.map(p => p.type)))
@@ -68,7 +68,7 @@ export default function PublicationsPage() {
6868
return (
6969
<div className="flex flex-col">
7070
{/* Header */}
71-
<section className="bg-muted/30 py-24">
71+
<section className="bg-muted/30 pb-12 pt-24 lg:pb-16">
7272
<div className="section-container">
7373
<div className="mx-auto max-w-2xl text-center">
7474
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl">
@@ -93,7 +93,8 @@ export default function PublicationsPage() {
9393
{/* Filters */}
9494
<section
9595
ref={filterRef}
96-
className="sticky top-16 z-10 border-b bg-background py-8"
96+
className="sticky top-16 z-10 border-b bg-background py-6 sm:py-8"
97+
style={headerOffset !== null ? { top: headerOffset } : undefined}
9798
>
9899
<div className="section-container">
99100
<div className="flex flex-col gap-4">
@@ -158,7 +159,7 @@ export default function PublicationsPage() {
158159
<div key={year} className="mb-16">
159160
<h2
160161
className="sticky top-32 mb-8 bg-background py-4 text-3xl font-bold"
161-
style={yearStickyOffset !== null ? { top: `${yearStickyOffset}px` } : undefined}
162+
style={yearStickyOffset !== null ? { top: yearStickyOffset } : undefined}
162163
>
163164
{year}
164165
</h2>

app/research/CopyLinkButton.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use client"
2+
3+
import { Link as LinkIcon } from "lucide-react"
4+
import { useState } from "react"
5+
6+
export function CopyLinkButton({ slug }: { slug: string }) {
7+
const [copied, setCopied] = useState(false)
8+
9+
const handleCopy = () => {
10+
const url = `${window.location.origin}/research#${slug}`
11+
navigator.clipboard.writeText(url).then(() => {
12+
setCopied(true)
13+
setTimeout(() => setCopied(false), 2000)
14+
})
15+
}
16+
17+
return (
18+
<button
19+
onClick={handleCopy}
20+
aria-label="Copy link to this section"
21+
title={copied ? "Copied!" : "Copy link"}
22+
className="inline-flex size-8 items-center justify-center rounded-lg border bg-background text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
23+
>
24+
<LinkIcon className="size-4" />
25+
</button>
26+
)
27+
}

app/research/ReadMoreSection.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"use client"
2+
3+
import { ChevronDown } from "lucide-react"
4+
import { useState } from "react"
5+
6+
export function ReadMoreSection({ text }: { text: string }) {
7+
const [isOpen, setIsOpen] = useState(false)
8+
9+
return (
10+
<div>
11+
<button
12+
onClick={() => setIsOpen(!isOpen)}
13+
className="group inline-flex items-center gap-2 text-sm font-medium text-primary transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
14+
aria-expanded={isOpen}
15+
>
16+
Read more
17+
<ChevronDown
18+
className={`size-4 transition-transform ${isOpen ? "rotate-180" : ""}`}
19+
aria-hidden="true"
20+
/>
21+
</button>
22+
{isOpen && (
23+
<p className="animate-in fade-in slide-in-from-top-2 mt-3 leading-relaxed text-muted-foreground duration-200">
24+
{text}
25+
</p>
26+
)}
27+
</div>
28+
)
29+
}

app/research/StickyLocalNav.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"use client"
2+
3+
import { useEffect, useState } from "react"
4+
5+
import { useHeaderOffset } from "@/lib/hooks/useHeaderOffset"
6+
7+
interface ResearchArea {
8+
id: string
9+
title: string
10+
slug: string
11+
description: string
12+
extended?: string
13+
featured?: boolean
14+
tools?: unknown[]
15+
image?: string | string[]
16+
}
17+
18+
export function StickyLocalNav({ areas }: { areas: ResearchArea[] }) {
19+
const [activeSlug, setActiveSlug] = useState<string>("")
20+
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false)
21+
const headerOffset = useHeaderOffset()
22+
23+
useEffect(() => {
24+
// Check for reduced motion preference
25+
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)")
26+
setPrefersReducedMotion(mediaQuery.matches)
27+
28+
const observer = new IntersectionObserver(
29+
(entries) => {
30+
entries.forEach((entry) => {
31+
if (entry.isIntersecting) {
32+
setActiveSlug(entry.target.id)
33+
}
34+
})
35+
},
36+
{ rootMargin: "-20% 0px -60% 0px", threshold: 0 }
37+
)
38+
39+
areas.forEach((area) => {
40+
const element = document.getElementById(area.slug)
41+
if (element) observer.observe(element)
42+
})
43+
44+
return () => observer.disconnect()
45+
}, [areas])
46+
47+
const scrollToSection = (slug: string) => {
48+
const element = document.getElementById(slug)
49+
if (element) {
50+
element.scrollIntoView({ behavior: prefersReducedMotion ? "auto" : "smooth" })
51+
}
52+
}
53+
54+
return (
55+
<nav
56+
className="sticky top-16 z-20 border-b border-border/60 bg-background/95 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-background/80"
57+
style={headerOffset !== null ? { top: headerOffset } : undefined}
58+
aria-label="Research navigation"
59+
>
60+
<div className="section-container">
61+
<div className="scrollbar-hide flex gap-1.5 overflow-x-auto py-2.5 sm:gap-2 sm:py-3">
62+
{areas.map((area, idx) => (
63+
<button
64+
key={area.slug}
65+
onClick={() => scrollToSection(area.slug)}
66+
data-active={activeSlug === area.slug}
67+
aria-current={activeSlug === area.slug ? "true" : undefined}
68+
className="inline-flex shrink-0 items-center gap-2 rounded-full border px-4 py-1.5 text-sm transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[active=true]:bg-foreground data-[active=true]:text-background"
69+
>
70+
{idx + 1}. {area.title}
71+
</button>
72+
))}
73+
</div>
74+
</div>
75+
</nav>
76+
)
77+
}

0 commit comments

Comments
 (0)