adjust filter in Programm page

This commit is contained in:
Alisa Cantillo-Olsson 2026-04-28 15:58:01 +02:00
parent 59a03518b5
commit f6db9f6134
7 changed files with 158 additions and 162 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@ -55,31 +55,31 @@ export const Banner15 = (props: Banner15Props) => {
export const Banner15Defaults: Props = { export const Banner15Defaults: Props = {
sections: [ sections: [
{ {
title: "Relume Library", title: "UX Schweiz",
image: { image: {
src: "https://relume-assets.s3.us-east-1.amazonaws.com/placeholder-image.svg", src: "https://relume-assets.s3.us-east-1.amazonaws.com/placeholder-image.svg",
alt: "Relume Library", alt: "UX Schweiz",
}, },
}, },
{ {
title: "Relume Library", title: "Adobe",
image: { image: {
src: "https://relume-assets.s3.us-east-1.amazonaws.com/placeholder-image.svg", src: "https://relume-assets.s3.us-east-1.amazonaws.com/placeholder-image.svg",
alt: "Relume Library", alt: "Adobe",
}, },
}, },
{ {
title: "Relume Library", title: "Google",
image: { image: {
src: "https://relume-assets.s3.us-east-1.amazonaws.com/placeholder-image.svg", src: "https://relume-assets.s3.us-east-1.amazonaws.com/placeholder-image.svg",
alt: "Relume Library", alt: "Google",
}, },
}, },
{ {
title: "Relume Library", title: "Figma",
image: { image: {
src: "https://relume-assets.s3.us-east-1.amazonaws.com/placeholder-image.svg", src: "https://relume-assets.s3.us-east-1.amazonaws.com/placeholder-image.svg",
alt: "Relume Library", alt: "Figma",
}, },
}, },
], ],

View File

@ -23,29 +23,28 @@ type Props = {
}; };
const levelStyles: Record<EventLevel, string> = { const levelStyles: Record<EventLevel, string> = {
Senior: "bg-electric-violet text-cloud-white", Senior: "bg-electric-violet/20 text-electric-violet",
"Mid-Level": "bg-electric-violet text-cloud-white", "Mid-Level": "bg-electric-violet/20 text-electric-violet",
Junior: "bg-acid-lime text-tech-navy", Junior: "bg-electric-violet/20 text-electric-violet",
}; };
const EventRow = ({ event }: { event: ProgramEvent }) => ( const EventRow = ({ event }: { event: ProgramEvent }) => (
<div className="grid grid-cols-1 items-center gap-4 border-t border-gray-200 py-6 last-of-type:border-b md:grid-cols-[5.5rem_5.5rem_1fr_max-content_max-content_max-content] md:gap-6 md:py-7"> <div className="grid grid-cols-1 items-center gap-4 border-t border-gray-200 py-6 last-of-type:border-b md:grid-cols-[5.5rem_5.5rem_1fr_max-content_max-content] md:gap-6 md:py-7">
<span className="text-sm font-semibold text-tech-navy">{event.time}</span> <span className="text-sm font-semibold text-tech-navy">{event.time}</span>
<div className="w-full aspect-square md:w-[5.5rem]"> <div className="w-14 h-14 shrink-0 md:w-[5.5rem] md:h-[5.5rem]">
<img src={event.image.src} alt={event.image.alt} className="size-full object-cover" /> <img src={event.image.src} alt={event.image.alt} className="size-full object-cover" />
</div> </div>
<div> <div className="flex flex-col gap-1.5">
<a href={`/programm/${event.id}`} className="hover:underline"> <a href={`/programm/${event.id}`} className="hover:underline">
<h5 className="text-lg font-bold text-tech-navy">{event.title}</h5> <h5 className="text-lg font-bold text-tech-navy">{event.title}</h5>
</a> </a>
<p className="text-sm text-gray-500">{event.speaker}</p> <p className="text-sm text-gray-500">{event.speaker}</p>
</div> <span className={`w-fit rounded-md px-3 py-1 text-xs font-semibold ${levelStyles[event.level]}`}>
<span className={`justify-self-start rounded-full px-3 py-1 text-xs font-semibold md:justify-self-auto ${levelStyles[event.level]}`}>
{event.level} {event.level}
</span> </span>
</div>
<a <a
href={event.speakerUrl} href={event.speakerUrl}

View File

@ -2,7 +2,6 @@ import { useState } from "react";
import { BiFilter, BiSearch } from "react-icons/bi"; import { BiFilter, BiSearch } from "react-icons/bi";
import { IoCloseOutline } from "react-icons/io5"; import { IoCloseOutline } from "react-icons/io5";
import { RxChevronDown } from "react-icons/rx"; import { RxChevronDown } from "react-icons/rx";
import { AnimatePresence, motion } from "framer-motion";
export type FilterTab = { value: string; label: string }; export type FilterTab = { value: string; label: string };
type ActiveTag = { label: string; onRemove: () => void }; type ActiveTag = { label: string; onRemove: () => void };
@ -58,16 +57,18 @@ export const Filters5 = ({
activeDay = "all", activeDay = "all",
onDayChange = () => {}, onDayChange = () => {},
}: Props) => { }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [sortOpen, setSortOpen] = useState(false); const [sortOpen, setSortOpen] = useState(false);
const [dayOpen, setDayOpen] = useState(false);
const TabRow = () => ( const selectedDayLabel = DAYS.find((d) => d.value === activeDay)?.label ?? "Alle Tage";
const TabPills = () => (
<div className="flex items-center gap-1 overflow-x-auto no-scrollbar"> <div className="flex items-center gap-1 overflow-x-auto no-scrollbar">
{tabs.map((tab) => ( {tabs.map((tab) => (
<button <button
key={tab.value} key={tab.value}
onClick={() => onTabChange(tab.value)} onClick={() => onTabChange(tab.value)}
className={`shrink-0 rounded-full px-4 py-1.5 text-sm font-semibold transition-colors ${ className={`shrink-0 rounded-2xl px-4 py-2 text-base font-semibold transition-colors ${
activeTab === tab.value activeTab === tab.value
? "bg-electric-violet text-cloud-white" ? "bg-electric-violet text-cloud-white"
: "text-tech-navy hover:text-electric-violet" : "text-tech-navy hover:text-electric-violet"
@ -79,46 +80,44 @@ export const Filters5 = ({
</div> </div>
); );
const SearchRow = () => ( // Speaker page: just tabs + search, no day filter or sort
<div className="mt-4"> if (!useFilterToggle) {
<div className="mb-1 flex items-center justify-between"> return (
<span className="text-sm font-semibold text-tech-navy">{searchLabel}</span> <section className="bg-cloud-white font-barlow border-b border-gray-200 px-[5%] py-8">
<button <div className="container flex flex-col gap-6">
onClick={onReset} <TabPills />
className="text-sm font-semibold text-electric-violet underline" <div className="flex flex-col gap-2">
> <div className="flex items-center justify-between">
<span className="text-base font-medium text-tech-navy">{searchLabel}</span>
<button onClick={onReset} className="text-base font-medium text-tech-navy">
{resetLabel} {resetLabel}
</button> </button>
</div> </div>
<div className="flex items-center gap-2 rounded-full border border-electric-violet px-4 py-2 text-sm"> <div className="flex h-12 items-center gap-3 rounded-2xl border border-electric-violet px-3">
<BiSearch className="size-4 shrink-0 text-gray-400" /> <BiSearch className="size-6 shrink-0 text-electric-violet" />
<input <input
className="flex-1 bg-transparent outline-none placeholder-gray-400 text-sm" className="flex-1 bg-transparent text-base text-electric-violet outline-none placeholder:text-electric-violet/50"
placeholder="Suchen..." placeholder="Suchen..."
value={searchQuery} value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)} onChange={(e) => onSearchChange(e.target.value)}
/> />
{searchQuery && (
<button onClick={() => onSearchChange("")}>
<IoCloseOutline className="size-5 text-electric-violet" />
</button>
)}
</div> </div>
</div> </div>
);
// Speaker variant: always-visible search, no toggle
if (!useFilterToggle) {
return (
<section className="px-[5%] py-8 bg-cloud-white font-barlow border-b border-gray-100">
<div className="container">
<TabRow />
<SearchRow />
{activeTags.length > 0 && ( {activeTags.length > 0 && (
<div className="mt-3 flex flex-wrap gap-2"> <div className="flex flex-wrap gap-3">
{activeTags.map((tag, i) => ( {activeTags.map((tag, i) => (
<span <span
key={i} key={i}
className="flex items-center gap-1 rounded-full border border-electric-violet px-3 py-1 text-sm font-semibold text-electric-violet" className="flex items-center gap-2 rounded-lg bg-neutral-dark pl-4 pr-3 py-2 text-base text-cloud-white"
> >
{tag.label} {tag.label}
<button onClick={tag.onRemove}> <button onClick={tag.onRemove}>
<IoCloseOutline className="size-4" /> <IoCloseOutline className="size-[18px]" />
</button> </button>
</span> </span>
))} ))}
@ -129,36 +128,39 @@ export const Filters5 = ({
); );
} }
// Programm variant: toggle button, collapsible panel // Programm page: full filter bar, always visible
return ( return (
<section className="px-[5%] py-8 bg-cloud-white font-barlow border-b border-gray-100"> <section className="bg-cloud-white font-barlow px-[5%] py-8">
<div className="container"> <div className="container flex flex-col gap-8">
<div className="flex items-center gap-4 overflow-x-auto no-scrollbar">
<button {/* Row 1: Filter label + category pills + sort */}
onClick={() => setIsOpen((v) => !v)} <div className="flex items-center gap-4">
className="flex shrink-0 items-center gap-2 rounded-full bg-electric-violet px-4 py-2 text-sm font-semibold text-cloud-white" <div className="flex shrink-0 items-center gap-3 rounded-2xl bg-electric-violet px-6 py-3 text-base font-semibold text-cloud-white">
> <BiFilter className="size-6" />
<BiFilter className="size-5" />
Filter Filter
</button> </div>
<TabRow />
<div className="flex flex-1 items-center overflow-x-auto no-scrollbar">
<TabPills />
</div>
{showSortButton && ( {showSortButton && (
<div className="relative ml-auto shrink-0"> <div className="relative shrink-0">
<button <button
onClick={() => setSortOpen((v) => !v)} onClick={() => setSortOpen((v) => !v)}
className="flex items-center gap-1 text-sm font-semibold text-electric-violet" className="flex items-center gap-2 text-base font-semibold text-electric-violet"
> >
Sortieren{" "} Sortieren
<RxChevronDown <RxChevronDown
className={`transition-transform ${sortOpen ? "rotate-180" : ""}`} className={`size-6 transition-transform ${sortOpen ? "rotate-180" : ""}`}
/> />
</button> </button>
{sortOpen && ( {sortOpen && (
<div className="absolute right-0 top-8 z-50 w-44 border border-gray-200 bg-white shadow-sm"> <div className="absolute right-0 top-9 z-50 w-48 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-md">
{["Zeit aufsteigend", "Zeit absteigend"].map((opt) => ( {["Zeit aufsteigend", "Zeit absteigend"].map((opt) => (
<button <button
key={opt} key={opt}
className="block w-full px-4 py-2 text-left text-sm hover:bg-gray-50" className="block w-full px-4 py-2.5 text-left text-sm text-tech-navy hover:bg-electric-violet/10"
onClick={() => setSortOpen(false)} onClick={() => setSortOpen(false)}
> >
{opt} {opt}
@ -170,68 +172,89 @@ export const Filters5 = ({
)} )}
</div> </div>
<AnimatePresence initial={false}> {/* Row 2: Search + Day filter — always visible */}
{isOpen && ( <div className="grid grid-cols-1 gap-8 md:grid-cols-2">
<motion.div
initial={{ height: 0, opacity: 0 }} {/* Search */}
animate={{ height: "auto", opacity: 1 }} <div className="flex flex-col gap-2">
exit={{ height: 0, opacity: 0 }} <div className="flex items-center justify-between">
transition={{ duration: 0.2, ease: "easeInOut" }} <span className="text-base font-medium text-tech-navy">{searchLabel}</span>
className="overflow-hidden" <button onClick={onReset} className="text-base font-medium text-tech-navy">
> {resetLabel}
<div className="mt-4 flex flex-wrap items-end gap-4"> </button>
<div className="flex min-w-[220px] flex-1 items-center gap-2 rounded-full border border-electric-violet px-4 py-2 text-sm"> </div>
<BiSearch className="size-4 shrink-0 text-gray-400" /> <div className="flex h-12 items-center gap-3 rounded-2xl border border-electric-violet px-3">
<BiSearch className="size-6 shrink-0 text-electric-violet" />
<input <input
className="flex-1 bg-transparent outline-none placeholder-gray-400 text-sm" className="flex-1 bg-transparent text-base text-electric-violet outline-none placeholder:text-electric-violet/50"
placeholder={searchLabel} placeholder="Titel oder Speaker..."
value={searchQuery} value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)} onChange={(e) => onSearchChange(e.target.value)}
/> />
</div> {searchQuery && (
<button <button onClick={() => onSearchChange("")}>
onClick={onReset} <IoCloseOutline className="size-5 text-electric-violet" />
className="shrink-0 text-sm font-semibold text-electric-violet underline"
>
{resetLabel}
</button> </button>
)}
</div>
</div>
{/* Day dropdown */}
{showDayFilter && ( {showDayFilter && (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-2">
<span className="text-xs font-semibold text-gray-500"> <span className="text-base font-medium text-tech-navy">Filtern nach Eventtage</span>
Filtern nach Eventtage <div className="relative">
</span> <button
<select onClick={() => setDayOpen((v) => !v)}
value={activeDay} className="flex h-12 w-full items-center justify-between gap-4 rounded-2xl border border-electric-violet px-4 text-base text-electric-violet"
onChange={(e) => onDayChange(e.target.value)}
className="rounded-full border border-electric-violet px-4 py-2 text-sm font-semibold text-tech-navy outline-none"
> >
<span>{selectedDayLabel}</span>
<RxChevronDown
className={`size-6 shrink-0 transition-transform ${dayOpen ? "rotate-180" : ""}`}
/>
</button>
{dayOpen && (
<div className="absolute left-0 top-[calc(100%+4px)] z-50 w-full overflow-hidden rounded-2xl border border-electric-violet bg-white shadow-md">
{DAYS.map((d) => ( {DAYS.map((d) => (
<option key={d.value} value={d.value}> <button
key={d.value}
className={`block w-full px-4 py-2.5 text-left text-base transition-colors hover:bg-electric-violet/10 ${
activeDay === d.value
? "font-semibold text-electric-violet"
: "text-tech-navy"
}`}
onClick={() => {
onDayChange(d.value);
setDayOpen(false);
}}
>
{d.label} {d.label}
</option> </button>
))} ))}
</select>
</div> </div>
)} )}
</div> </div>
</div>
)}
</div>
{/* Active tags */}
{activeTags.length > 0 && ( {activeTags.length > 0 && (
<div className="mt-3 flex flex-wrap gap-2"> <div className="flex flex-wrap gap-3">
{activeTags.map((tag, i) => ( {activeTags.map((tag, i) => (
<span <span
key={i} key={i}
className="flex items-center gap-1 rounded-full border border-electric-violet px-3 py-1 text-sm font-semibold text-electric-violet" className="flex items-center gap-2 rounded-lg bg-neutral-dark pl-4 pr-3 py-2 text-base text-cloud-white"
> >
{tag.label} {tag.label}
<button onClick={tag.onRemove}> <button onClick={tag.onRemove}>
<IoCloseOutline className="size-4" /> <IoCloseOutline className="size-[18px]" />
</button> </button>
</span> </span>
))} ))}
</div> </div>
)} )}
</motion.div>
)}
</AnimatePresence>
</div> </div>
</section> </section>
); );

View File

@ -2,6 +2,8 @@ import speaker1 from "../assets/speakers/speaker1.jpg";
import speaker2 from "../assets/speakers/speaker2.jpg"; import speaker2 from "../assets/speakers/speaker2.jpg";
import speaker3 from "../assets/speakers/speaker3.jpg"; import speaker3 from "../assets/speakers/speaker3.jpg";
import speaker4 from "../assets/speakers/speaker4.jpg"; import speaker4 from "../assets/speakers/speaker4.jpg";
import speaker5 from "../assets/speakers/speaker5.jpg";
import speaker6 from "../assets/speakers/speaker6.jpg";
import { BiLogoLinkedinSquare } from "react-icons/bi"; import { BiLogoLinkedinSquare } from "react-icons/bi";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@ -173,7 +175,7 @@ export const Team4Defaults: Props = {
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }], socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
}, },
{ {
image: { src: speaker1, alt: "Liam O'Connor" }, image: { src: speaker5, alt: "Liam O'Connor" },
name: "Liam O'Connor", name: "Liam O'Connor",
role: "Lead Product Designer", role: "Lead Product Designer",
company: "Spotify", company: "Spotify",
@ -188,7 +190,7 @@ export const Team4Defaults: Props = {
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }], socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
}, },
{ {
image: { src: speaker2, alt: "Fabienne Keller" }, image: { src: speaker6, alt: "Fabienne Keller" },
name: "Fabienne Keller", name: "Fabienne Keller",
role: "Gründerin", role: "Gründerin",
company: "User-First Agency", company: "User-First Agency",
@ -202,36 +204,6 @@ export const Team4Defaults: Props = {
talks: [{ title: "Workshop: Research Repositories", type: "Workshop" }], talks: [{ title: "Workshop: Research Repositories", type: "Workshop" }],
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }], socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
}, },
{
image: { src: speaker3, alt: "Thomas Meyer" },
name: 'Thomas "Tom" Meyer',
role: "Creative Director",
company: "Swisscom",
slug: "thomas-meyer",
location: "Bern, Schweiz",
description:
"Tom zeigt, wie man in großen Konzernen eine Design-Kultur etabliert, die über das visuelle Layer hinausgeht.",
fullBio:
'Thomas "Tom" Meyer ist Creative Director bei Swisscom und zeigt, wie man in großen Konzernen eine Design-Kultur etabliert, die über das visuelle Layer hinausgeht. Er bringt über 15 Jahre Erfahrung im Corporate Design Management mit.',
category: "Strategie",
talks: [{ title: "Networking: Leading Teams", type: "Networking" }],
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
},
{
image: { src: speaker4, alt: "Sarah M. Widmer" },
name: "Sarah M. Widmer",
role: "Senior UX Architect",
company: "SBB",
slug: "sarah-m-widmer",
location: "Bern, Schweiz",
description:
"Sarah gestaltet die Mobilität von morgen. Sie zeigt, wie man Millionen von Nutzern barrierefrei durch den digitalen öV-Dschungel leitet.",
fullBio:
"Sarah M. Widmer ist Senior UX Architect bei der SBB und gestaltet die Mobilität von morgen. Sie zeigt, wie man Millionen von Nutzern barrierefrei durch den digitalen öV-Dschungel leitet und dabei Accessibility nicht als Pflicht, sondern als Designprinzip versteht.",
category: "Design / UX",
talks: [{ title: "Talk: Inclusive Design at Scale", type: "Talk" }],
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
},
], ],
}; };

View File

@ -2,6 +2,8 @@ import spk1 from "../assets/speakers/speaker1.jpg";
import spk2 from "../assets/speakers/speaker2.jpg"; import spk2 from "../assets/speakers/speaker2.jpg";
import spk3 from "../assets/speakers/speaker3.jpg"; import spk3 from "../assets/speakers/speaker3.jpg";
import spk4 from "../assets/speakers/speaker4.jpg"; import spk4 from "../assets/speakers/speaker4.jpg";
import spk5 from "../assets/speakers/speaker5.jpg";
import spk6 from "../assets/speakers/speaker6.jpg";
import type { ProgramEvent } from "../components/Event33"; import type { ProgramEvent } from "../components/Event33";
export const ALL_EVENTS: ProgramEvent[] = [ export const ALL_EVENTS: ProgramEvent[] = [
@ -69,7 +71,7 @@ export const ALL_EVENTS: ProgramEvent[] = [
{ {
id: "do-5", id: "do-5",
time: "18:00 19:30 Uhr", time: "18:00 19:30 Uhr",
image: { src: spk1, alt: "Liam O'Connor" }, image: { src: spk5, alt: "Liam O'Connor" },
title: "Talk: Ethical AI", title: "Talk: Ethical AI",
subtitle: "Verantwortungsvolles Design in der KI-Ära", subtitle: "Verantwortungsvolles Design in der KI-Ära",
speaker: "Liam O'Connor", speaker: "Liam O'Connor",
@ -145,7 +147,7 @@ export const ALL_EVENTS: ProgramEvent[] = [
{ {
id: "fr-5", id: "fr-5",
time: "18:00 19:30 Uhr", time: "18:00 19:30 Uhr",
image: { src: spk1, alt: "Liam O'Connor" }, image: { src: spk5, alt: "Liam O'Connor" },
title: "Talk: Trust in AI", title: "Talk: Trust in AI",
subtitle: "Wie wir Vertrauen in algorithmische Systeme aufbauen", subtitle: "Wie wir Vertrauen in algorithmische Systeme aufbauen",
speaker: "Liam O'Connor", speaker: "Liam O'Connor",
@ -160,7 +162,7 @@ export const ALL_EVENTS: ProgramEvent[] = [
{ {
id: "fr-6", id: "fr-6",
time: "20:00 21:30 Uhr", time: "20:00 21:30 Uhr",
image: { src: spk2, alt: "Fabienne Keller" }, image: { src: spk6, alt: "Fabienne Keller" },
title: "Workshop: Research Repositories", title: "Workshop: Research Repositories",
subtitle: "Wissen organisieren, das wirklich genutzt wird", subtitle: "Wissen organisieren, das wirklich genutzt wird",
speaker: "Fabienne Keller", speaker: "Fabienne Keller",