update programm and speaker pages with filters and new layouts
This commit is contained in:
parent
3ed08a71a6
commit
9d5f1fc841
@ -1,283 +1,79 @@
|
|||||||
import { Button, Tabs, TabsContent, TabsList, TabsTrigger } from "@relume_io/relume-ui";
|
|
||||||
import type { ButtonProps } from "@relume_io/relume-ui";
|
|
||||||
import { BiMap } from "react-icons/bi";
|
|
||||||
|
|
||||||
type ImageProps = {
|
export type EventLevel = "Senior" | "Mid-Level" | "Junior";
|
||||||
src: string;
|
export type EventType = "Talk" | "Workshop" | "Interaction" | "AI and Future";
|
||||||
alt?: string;
|
export type EventDay = "Donnerstag" | "Freitag";
|
||||||
};
|
|
||||||
|
|
||||||
type ScheduledEvent = {
|
export type ProgramEvent = {
|
||||||
url: string;
|
id: string;
|
||||||
time: string;
|
time: string;
|
||||||
image: ImageProps;
|
image: { src: string; alt: string };
|
||||||
title: string;
|
title: string;
|
||||||
speaker: string;
|
speaker: string;
|
||||||
location: string;
|
type: EventType;
|
||||||
button: ButtonProps;
|
level: EventLevel;
|
||||||
};
|
day: EventDay;
|
||||||
|
speakerUrl: string;
|
||||||
type Tab = {
|
|
||||||
value: string;
|
|
||||||
trigger: string;
|
|
||||||
content: ScheduledEvent[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tagline: string;
|
events: ProgramEvent[];
|
||||||
heading: string;
|
|
||||||
description: string;
|
|
||||||
tabs: Tab[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Event33Props = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;
|
const levelStyles: Record<EventLevel, string> = {
|
||||||
|
Senior: "bg-electric-violet text-cloud-white",
|
||||||
|
"Mid-Level": "bg-electric-violet text-cloud-white",
|
||||||
|
Junior: "bg-acid-lime text-tech-navy",
|
||||||
|
};
|
||||||
|
|
||||||
export const Event33 = (props: Event33Props) => {
|
const EventRow = ({ event }: { event: ProgramEvent }) => (
|
||||||
const { tagline, heading, description, tabs } = {
|
<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">
|
||||||
...Event33Defaults,
|
<span className="text-sm font-semibold text-tech-navy">{event.time}</span>
|
||||||
...props,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
<div className="w-full aspect-square md:w-[5.5rem]">
|
||||||
|
<img src={event.image.src} alt={event.image.alt} className="size-full object-cover" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h5 className="text-lg font-bold text-tech-navy">{event.title}</h5>
|
||||||
|
<p className="text-sm text-gray-500">{event.speaker}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className={`justify-self-start rounded-full px-3 py-1 text-xs font-semibold md:justify-self-auto ${levelStyles[event.level]}`}>
|
||||||
|
{event.level}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={event.speakerUrl}
|
||||||
|
className="shrink-0 rounded-full border border-electric-violet px-4 py-1.5 text-sm font-semibold text-electric-violet hover:bg-electric-violet hover:text-cloud-white transition-colors"
|
||||||
|
>
|
||||||
|
Über {event.speaker.split(" ")[0]} {event.speaker.split(" ").slice(-1)[0]}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="/tickets"
|
||||||
|
className="shrink-0 rounded-full bg-electric-violet px-4 py-1.5 text-sm font-semibold text-cloud-white hover:opacity-90 transition-opacity"
|
||||||
|
>
|
||||||
|
Tickets kaufen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Event33 = ({ events }: Props) => {
|
||||||
return (
|
return (
|
||||||
<section id="relume" className="px-[5%] py-16 md:py-24 lg:py-28 bg-cloud-white font-barlow">
|
<section className="px-[5%] py-10 bg-cloud-white font-barlow">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="mb-12 md:mb-18 lg:mb-20">
|
{events.length === 0 ? (
|
||||||
<div className="max-w-lg">
|
<p className="py-16 text-center text-gray-400">Keine Veranstaltungen gefunden.</p>
|
||||||
<p className="mb-3 font-semibold md:mb-4">{tagline}</p>
|
) : (
|
||||||
<h1 className="mb-5 text-5xl font-bold md:mb-6 md:text-7xl lg:text-8xl">{heading}</h1>
|
<div>
|
||||||
<p className="md:text-md">{description}</p>
|
{events.map((event) => (
|
||||||
</div>
|
<EventRow key={event.id} event={event} />
|
||||||
</div>
|
|
||||||
<Tabs defaultValue={tabs[0].value} className="flex flex-col justify-start">
|
|
||||||
<TabsList className="no-scrollbar mb-12 ml-[-5vw] flex w-screen items-center overflow-auto pl-[5vw] md:ml-0 md:w-full md:overflow-hidden md:pl-0">
|
|
||||||
{tabs.map((tab, index) => (
|
|
||||||
<TabsTrigger
|
|
||||||
key={index}
|
|
||||||
value={tab.value}
|
|
||||||
className="px-4 data-[state=active]:border data-[state=active]:border-border-primary data-[state=inactive]:border-transparent data-[state=active]:bg-transparent data-[state=active]:text-neutral-black"
|
|
||||||
>
|
|
||||||
{tab.trigger}
|
|
||||||
</TabsTrigger>
|
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</div>
|
||||||
{tabs.map((tab) => (
|
)}
|
||||||
<TabsContent
|
|
||||||
key={tab.value}
|
|
||||||
value={tab.value}
|
|
||||||
className="data-[state=active]:animate-tabs"
|
|
||||||
>
|
|
||||||
{tab.content.map((event, index) => (
|
|
||||||
<ScheduledEvent key={index} {...event} />
|
|
||||||
))}
|
|
||||||
</TabsContent>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ScheduledEvent: React.FC<ScheduledEvent> = ({
|
export default Event33;
|
||||||
url,
|
|
||||||
time,
|
|
||||||
image,
|
|
||||||
title,
|
|
||||||
speaker,
|
|
||||||
location,
|
|
||||||
button,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 items-center gap-4 border-t border-border-primary py-6 last-of-type:border-b md:grid-cols-[6rem_max-content_1fr_max-content] md:gap-8 md:py-8">
|
|
||||||
<div className="text-md md:text-lg">{time}</div>
|
|
||||||
<div className="w-full md:w-36">
|
|
||||||
<a href={url} className="relative block aspect-[3/2] md:aspect-square">
|
|
||||||
<img src={image.src} alt={image.alt} className="absolute size-full object-cover" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div className="grid auto-cols-fr grid-cols-1 items-center gap-4 lg:grid-cols-[1fr_.25fr]">
|
|
||||||
<div>
|
|
||||||
<h5 className="text-xl font-bold md:text-2xl">{title}</h5>
|
|
||||||
<div>{speaker}</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm">
|
|
||||||
<BiMap className="size-6 flex-none" />
|
|
||||||
<span>{location}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button {...button} asChild>
|
|
||||||
<a href={url}>{button.title}</a>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Event33Defaults: Props = {
|
|
||||||
tagline: "Tagline",
|
|
||||||
heading: "Schedule",
|
|
||||||
description:
|
|
||||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse varius enim in eros elementum tristique.",
|
|
||||||
tabs: [
|
|
||||||
{
|
|
||||||
value: "fri-09-feb",
|
|
||||||
trigger: "Fri 09 Feb",
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
url: "#",
|
|
||||||
time: "8:00 am",
|
|
||||||
image: {
|
|
||||||
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
|
|
||||||
alt: "Relume placeholder image 1",
|
|
||||||
},
|
|
||||||
title: "Event title heading",
|
|
||||||
speaker: "Speaker",
|
|
||||||
location: "Location",
|
|
||||||
button: {
|
|
||||||
variant: "secondary",
|
|
||||||
size: "primary",
|
|
||||||
title: "View details",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "#",
|
|
||||||
time: "9:00 am",
|
|
||||||
image: {
|
|
||||||
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
|
|
||||||
alt: "Relume placeholder image 2",
|
|
||||||
},
|
|
||||||
title: "Event title heading",
|
|
||||||
speaker: "Speaker",
|
|
||||||
location: "Location",
|
|
||||||
button: {
|
|
||||||
variant: "secondary",
|
|
||||||
size: "primary",
|
|
||||||
title: "View details",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "#",
|
|
||||||
time: "10:00 am",
|
|
||||||
image: {
|
|
||||||
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
|
|
||||||
alt: "Relume placeholder image 3",
|
|
||||||
},
|
|
||||||
title: "Event title heading",
|
|
||||||
speaker: "Speaker",
|
|
||||||
location: "Location",
|
|
||||||
button: {
|
|
||||||
variant: "secondary",
|
|
||||||
size: "primary",
|
|
||||||
title: "View details",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "sat-10-feb",
|
|
||||||
trigger: "Sat 10 Feb",
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
url: "#",
|
|
||||||
time: "8:00 am",
|
|
||||||
image: {
|
|
||||||
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
|
|
||||||
alt: "Relume placeholder image 4",
|
|
||||||
},
|
|
||||||
title: "Event title heading",
|
|
||||||
speaker: "Speaker",
|
|
||||||
location: "Location",
|
|
||||||
button: {
|
|
||||||
variant: "secondary",
|
|
||||||
size: "primary",
|
|
||||||
title: "View details",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "#",
|
|
||||||
time: "9:00 am",
|
|
||||||
image: {
|
|
||||||
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
|
|
||||||
alt: "Relume placeholder image 5",
|
|
||||||
},
|
|
||||||
title: "Event title heading",
|
|
||||||
speaker: "Speaker",
|
|
||||||
location: "Location",
|
|
||||||
button: {
|
|
||||||
variant: "secondary",
|
|
||||||
size: "primary",
|
|
||||||
title: "View details",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "#",
|
|
||||||
time: "10:00 am",
|
|
||||||
image: {
|
|
||||||
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
|
|
||||||
alt: "Relume placeholder image 6",
|
|
||||||
},
|
|
||||||
title: "Event title heading",
|
|
||||||
speaker: "Speaker",
|
|
||||||
location: "Location",
|
|
||||||
button: {
|
|
||||||
variant: "secondary",
|
|
||||||
size: "primary",
|
|
||||||
title: "View details",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "sun-11-feb",
|
|
||||||
trigger: "Sun 11 Feb",
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
url: "#",
|
|
||||||
time: "8:00 am",
|
|
||||||
image: {
|
|
||||||
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
|
|
||||||
alt: "Relume placeholder image 7",
|
|
||||||
},
|
|
||||||
title: "Event title heading",
|
|
||||||
speaker: "Speaker",
|
|
||||||
location: "Location",
|
|
||||||
button: {
|
|
||||||
variant: "secondary",
|
|
||||||
size: "primary",
|
|
||||||
title: "View details",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "#",
|
|
||||||
time: "9:00 am",
|
|
||||||
image: {
|
|
||||||
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
|
|
||||||
alt: "Relume placeholder image 8",
|
|
||||||
},
|
|
||||||
title: "Event title heading",
|
|
||||||
speaker: "Speaker",
|
|
||||||
location: "Location",
|
|
||||||
button: {
|
|
||||||
variant: "secondary",
|
|
||||||
size: "primary",
|
|
||||||
title: "View details",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "#",
|
|
||||||
time: "10:00 am",
|
|
||||||
image: {
|
|
||||||
src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg",
|
|
||||||
alt: "Relume placeholder image 9",
|
|
||||||
},
|
|
||||||
title: "Event title heading",
|
|
||||||
speaker: "Speaker",
|
|
||||||
location: "Location",
|
|
||||||
button: {
|
|
||||||
variant: "secondary",
|
|
||||||
size: "primary",
|
|
||||||
title: "View details",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,316 +1,240 @@
|
|||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { BiFilter, BiSearch } from "react-icons/bi";
|
import { BiFilter, BiSearch } from "react-icons/bi";
|
||||||
import { RxChevronDown } from "react-icons/rx";
|
|
||||||
import { IoCloseOutline } from "react-icons/io5";
|
import { IoCloseOutline } from "react-icons/io5";
|
||||||
|
import { RxChevronDown } from "react-icons/rx";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import type { Variants } from "framer-motion";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
Tabs,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
} from "@relume_io/relume-ui";
|
|
||||||
|
|
||||||
type FilterValues = {
|
export type FilterTab = { value: string; label: string };
|
||||||
filterOne: string;
|
type ActiveTag = { label: string; onRemove: () => void };
|
||||||
filterTwo: string[];
|
|
||||||
filterThree: string[];
|
type Props = {
|
||||||
filterFour: string[];
|
tabs?: FilterTab[];
|
||||||
|
activeTab?: string;
|
||||||
|
onTabChange?: (tab: string) => void;
|
||||||
|
searchQuery?: string;
|
||||||
|
onSearchChange?: (q: string) => void;
|
||||||
|
searchLabel?: string;
|
||||||
|
resetLabel?: string;
|
||||||
|
activeTags?: ActiveTag[];
|
||||||
|
onReset?: () => void;
|
||||||
|
// Programm-only
|
||||||
|
useFilterToggle?: boolean;
|
||||||
|
showDayFilter?: boolean;
|
||||||
|
showSortButton?: boolean;
|
||||||
|
activeDay?: string;
|
||||||
|
onDayChange?: (day: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs = [
|
const PROGRAMM_TABS: FilterTab[] = [
|
||||||
{ value: "View all", trigger: "View all" },
|
{ value: "all", label: "Alle Anzeigen" },
|
||||||
{ value: "Category one", trigger: "Category one" },
|
{ value: "Talk", label: "Talk" },
|
||||||
{ value: "Category two", trigger: "Category two" },
|
{ value: "Workshop", label: "Workshop" },
|
||||||
{ value: "Category three", trigger: "Category three" },
|
{ value: "Interaction", label: "Interaction" },
|
||||||
{ value: "Category four", trigger: "Category four" },
|
{ value: "AI and Future", label: "AI and Future" },
|
||||||
|
{ value: "Senior", label: "Senior Level" },
|
||||||
|
{ value: "Mid-Level", label: "Mid-Level" },
|
||||||
|
{ value: "Junior", label: "Junior-Level" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const options = [
|
const DAYS = [
|
||||||
{ value: "option-1", label: "Option one" },
|
{ value: "all", label: "Alle Tage" },
|
||||||
{ value: "option-2", label: "Option two" },
|
{ value: "Donnerstag", label: "Donnerstag" },
|
||||||
{ value: "option-3", label: "Option three" },
|
{ value: "Freitag", label: "Freitag" },
|
||||||
{ value: "option-4", label: "Option four" },
|
|
||||||
{ value: "option-5", label: "Option five" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const containerVariants: Variants = {
|
export const Filters5 = ({
|
||||||
initial: {
|
tabs = PROGRAMM_TABS,
|
||||||
height: 0,
|
activeTab = "all",
|
||||||
opacity: 0,
|
onTabChange = () => {},
|
||||||
transition: { duration: 0.2, ease: "easeInOut" },
|
searchQuery = "",
|
||||||
},
|
onSearchChange = () => {},
|
||||||
animate: {
|
searchLabel = "Stichwortsuche",
|
||||||
height: "auto",
|
resetLabel = "Zurücksetzen",
|
||||||
opacity: 1,
|
activeTags = [],
|
||||||
transition: { duration: 0.2, ease: "easeInOut" },
|
onReset = () => {},
|
||||||
},
|
useFilterToggle = true,
|
||||||
exit: {
|
showDayFilter = true,
|
||||||
height: 0,
|
showSortButton = true,
|
||||||
opacity: 0,
|
activeDay = "all",
|
||||||
transition: { duration: 0.2, ease: "easeInOut" },
|
onDayChange = () => {},
|
||||||
},
|
}: Props) => {
|
||||||
};
|
|
||||||
|
|
||||||
export const Filters5 = () => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [selectedValues, setSelectedValues] = useState<FilterValues>({
|
const [sortOpen, setSortOpen] = useState(false);
|
||||||
filterOne: "",
|
|
||||||
filterTwo: ["option-1"],
|
|
||||||
filterThree: ["option-1"],
|
|
||||||
filterFour: ["option-1"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleClear = (filter: keyof FilterValues) => {
|
const TabRow = () => (
|
||||||
setSelectedValues((prev) => ({
|
<div className="flex items-center gap-1 overflow-x-auto no-scrollbar">
|
||||||
...prev,
|
{tabs.map((tab) => (
|
||||||
[filter]: Array.isArray(prev[filter]) ? [] : "",
|
<button
|
||||||
}));
|
key={tab.value}
|
||||||
};
|
onClick={() => onTabChange(tab.value)}
|
||||||
|
className={`shrink-0 rounded-full px-4 py-1.5 text-sm font-semibold transition-colors ${
|
||||||
const handleCheckboxChange = (filter: keyof Omit<FilterValues, "filterOne">, value: string) => {
|
activeTab === tab.value
|
||||||
setSelectedValues((prev) => {
|
? "bg-electric-violet text-cloud-white"
|
||||||
const currentItems = prev[filter];
|
: "text-tech-navy hover:text-electric-violet"
|
||||||
return {
|
}`}
|
||||||
...prev,
|
|
||||||
[filter]: currentItems.includes(value)
|
|
||||||
? currentItems.filter((item) => item !== value)
|
|
||||||
: [...currentItems, value],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSelectedLabels = (filter: keyof FilterValues) => {
|
|
||||||
if (!selectedValues[filter].length) return "Select options";
|
|
||||||
if (selectedValues[filter].length === 1) {
|
|
||||||
return options.find((opt) => opt.value === selectedValues[filter][0])?.label;
|
|
||||||
}
|
|
||||||
return `${selectedValues[filter].length} selected`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="px-[5%] py-16 md:py-24 lg:py-28 bg-cloud-white font-barlow">
|
|
||||||
<div className="container">
|
|
||||||
<div className="mb-12 w-full text-center md:mb-18 lg:mb-20">
|
|
||||||
<h1 className="mb-5 text-5xl font-bold md:mb-6 md:text-7xl lg:text-8xl">Collection</h1>
|
|
||||||
<p className="md:text-md">Lorem ipsum dolor sit amet.</p>
|
|
||||||
</div>
|
|
||||||
<div className="mb-8 grid auto-cols-fr grid-cols-2 grid-rows-[auto_auto] items-center justify-between gap-x-6 gap-y-8 lg:grid-cols-[1fr_max-content_1fr] lg:gap-y-0">
|
|
||||||
<Button
|
|
||||||
className="w-fit gap-3 self-start bg-electric-violet text-cloud-white rounded-2xl px-6 py-3 border-electric-violet font-semibold"
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
>
|
>
|
||||||
<BiFilter className="size-6" />
|
{tab.label}
|
||||||
<span>Filter</span>
|
</button>
|
||||||
</Button>
|
))}
|
||||||
<Tabs
|
</div>
|
||||||
defaultValue={tabs[0].value}
|
);
|
||||||
className="no-scrollbar flex w-full flex-col items-center justify-center overflow-scroll [grid-area:2/1/3/3] md:overflow-auto lg:[grid-area:auto/auto/auto/auto]"
|
|
||||||
>
|
const SearchRow = () => (
|
||||||
<TabsList className="flex w-screen items-center justify-start pl-[5vw] md:w-full md:justify-center md:overflow-hidden md:pl-0">
|
<div className="mt-4">
|
||||||
{tabs.map((tab, index) => (
|
<div className="mb-1 flex items-center justify-between">
|
||||||
<TabsTrigger
|
<span className="text-sm font-semibold text-tech-navy">{searchLabel}</span>
|
||||||
key={index}
|
<button
|
||||||
value={tab.value}
|
onClick={onReset}
|
||||||
className="px-4 data-[state=active]:border data-[state=active]:border-electric-violet data-[state=inactive]:border-transparent data-[state=active]:bg-transparent data-[state=active]:text-electric-violet font-barlow"
|
className="text-sm font-semibold text-electric-violet underline"
|
||||||
|
>
|
||||||
|
{resetLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 rounded-full border border-electric-violet px-4 py-2 text-sm">
|
||||||
|
<BiSearch className="size-4 shrink-0 text-gray-400" />
|
||||||
|
<input
|
||||||
|
className="flex-1 bg-transparent outline-none placeholder-gray-400 text-sm"
|
||||||
|
placeholder="Suchen..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => onSearchChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</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 && (
|
||||||
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
{activeTags.map((tag, i) => (
|
||||||
|
<span
|
||||||
|
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"
|
||||||
>
|
>
|
||||||
{tab.trigger}
|
{tag.label}
|
||||||
</TabsTrigger>
|
<button onClick={tag.onRemove}>
|
||||||
|
<IoCloseOutline className="size-4" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</div>
|
||||||
</Tabs>
|
)}
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger className="-mr-2 flex items-center gap-2 justify-self-end">
|
|
||||||
<p className="whitespace-nowrap">Sort by</p>
|
|
||||||
<RxChevronDown className="shrink-0 text-text-primary transition-transform duration-300" />
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
{[
|
|
||||||
"Most Popular",
|
|
||||||
"Most Recent",
|
|
||||||
"Name: A to Z",
|
|
||||||
"Name: Z to A",
|
|
||||||
"Price: Low to High",
|
|
||||||
"Price: High to Low",
|
|
||||||
].map((option) => (
|
|
||||||
<DropdownMenuItem key={option}>{option}</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
<AnimatePresence initial={false} mode="wait">
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Programm variant: toggle button, collapsible panel
|
||||||
|
return (
|
||||||
|
<section className="px-[5%] py-8 bg-cloud-white font-barlow border-b border-gray-100">
|
||||||
|
<div className="container">
|
||||||
|
<div className="flex items-center gap-4 overflow-x-auto no-scrollbar">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen((v) => !v)}
|
||||||
|
className="flex shrink-0 items-center gap-2 rounded-full bg-electric-violet px-4 py-2 text-sm font-semibold text-cloud-white"
|
||||||
|
>
|
||||||
|
<BiFilter className="size-5" />
|
||||||
|
Filter
|
||||||
|
</button>
|
||||||
|
<TabRow />
|
||||||
|
{showSortButton && (
|
||||||
|
<div className="relative ml-auto shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => setSortOpen((v) => !v)}
|
||||||
|
className="flex items-center gap-1 text-sm font-semibold text-electric-violet"
|
||||||
|
>
|
||||||
|
Sortieren{" "}
|
||||||
|
<RxChevronDown
|
||||||
|
className={`transition-transform ${sortOpen ? "rotate-180" : ""}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{sortOpen && (
|
||||||
|
<div className="absolute right-0 top-8 z-50 w-44 border border-gray-200 bg-white shadow-sm">
|
||||||
|
{["Zeit aufsteigend", "Zeit absteigend"].map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt}
|
||||||
|
className="block w-full px-4 py-2 text-left text-sm hover:bg-gray-50"
|
||||||
|
onClick={() => setSortOpen(false)}
|
||||||
|
>
|
||||||
|
{opt}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AnimatePresence initial={false}>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={containerVariants}
|
initial={{ height: 0, opacity: 0 }}
|
||||||
initial="initial"
|
animate={{ height: "auto", opacity: 1 }}
|
||||||
animate="animate"
|
exit={{ height: 0, opacity: 0 }}
|
||||||
exit="exit"
|
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="mb-8 grid grid-cols-1 gap-x-8 gap-y-4 lg:grid-cols-4">
|
<div className="mt-4 flex flex-wrap items-end gap-4">
|
||||||
<div>
|
<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 className="mb-2 flex items-center justify-between">
|
<BiSearch className="size-4 shrink-0 text-gray-400" />
|
||||||
<span>Filter One</span>
|
<input
|
||||||
<Button
|
className="flex-1 bg-transparent outline-none placeholder-gray-400 text-sm"
|
||||||
variant="link"
|
placeholder={searchLabel}
|
||||||
size="link"
|
value={searchQuery}
|
||||||
onClick={() => handleClear("filterOne")}
|
onChange={(e) => onSearchChange(e.target.value)}
|
||||||
className="text-sm"
|
/>
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
placeholder="Stichwortsuche"
|
|
||||||
icon={<BiSearch className="size-6" />}
|
|
||||||
value={selectedValues.filterOne}
|
|
||||||
onChange={(e) =>
|
|
||||||
setSelectedValues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
filterOne: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="border-electric-violet rounded-2xl"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<button
|
||||||
<div className="mb-2 flex items-center justify-between">
|
onClick={onReset}
|
||||||
<span>Filter Two</span>
|
className="shrink-0 text-sm font-semibold text-electric-violet underline"
|
||||||
<Button
|
>
|
||||||
variant="link"
|
{resetLabel}
|
||||||
size="link"
|
</button>
|
||||||
onClick={() => handleClear("filterTwo")}
|
{showDayFilter && (
|
||||||
className="text-sm"
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="text-xs font-semibold text-gray-500">
|
||||||
|
Filtern nach Eventtage
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
value={activeDay}
|
||||||
|
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"
|
||||||
>
|
>
|
||||||
Clear
|
{DAYS.map((d) => (
|
||||||
</Button>
|
<option key={d.value} value={d.value}>
|
||||||
</div>
|
{d.label}
|
||||||
<Select value="" onValueChange={() => {}}>
|
</option>
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder={getSelectedLabels("filterTwo")} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{options.map((option) => (
|
|
||||||
<div key={option.value} className="flex items-center space-x-2 p-2">
|
|
||||||
<Checkbox
|
|
||||||
id={`filter-two-${option.value}`}
|
|
||||||
checked={selectedValues.filterTwo.includes(option.value)}
|
|
||||||
onCheckedChange={() => handleCheckboxChange("filterTwo", option.value)}
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
className="flex-grow cursor-pointer"
|
|
||||||
htmlFor={`filter-two-${option.value}`}
|
|
||||||
onClick={(e: React.MouseEvent<HTMLLabelElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleCheckboxChange("filterTwo", option.value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</select>
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mb-2 flex items-center justify-between">
|
|
||||||
<span>Filter Three</span>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
size="link"
|
|
||||||
onClick={() => handleClear("filterThree")}
|
|
||||||
className="text-sm"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<Select value="" onValueChange={() => {}}>
|
)}
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder={getSelectedLabels("filterThree")} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{options.map((option) => (
|
|
||||||
<div key={option.value} className="flex items-center space-x-2 px-2 py-1.5">
|
|
||||||
<Checkbox
|
|
||||||
id={`filter-three-${option.value}`}
|
|
||||||
checked={selectedValues.filterThree.includes(option.value)}
|
|
||||||
onCheckedChange={() =>
|
|
||||||
handleCheckboxChange("filterThree", option.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
className="flex-grow cursor-pointer"
|
|
||||||
htmlFor={`filter-three-${option.value}`}
|
|
||||||
onClick={(e: React.MouseEvent<HTMLLabelElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleCheckboxChange("filterThree", option.value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mb-2 flex items-center justify-between">
|
|
||||||
<span>Filter Four</span>
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
size="link"
|
|
||||||
onClick={() => handleClear("filterFour")}
|
|
||||||
className="text-sm"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Select value="" onValueChange={() => {}}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder={getSelectedLabels("filterFour")} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{options.map((option) => (
|
|
||||||
<div key={option.value} className="flex items-center space-x-2 px-2 py-1.5">
|
|
||||||
<Checkbox
|
|
||||||
id={`filter-four-${option.value}`}
|
|
||||||
checked={selectedValues.filterFour.includes(option.value)}
|
|
||||||
onCheckedChange={() => handleCheckboxChange("filterFour", option.value)}
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
className="flex-grow cursor-pointer"
|
|
||||||
htmlFor={`filter-four-${option.value}`}
|
|
||||||
onClick={(e: React.MouseEvent<HTMLLabelElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
handleCheckboxChange("filterFour", option.value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{activeTags.length > 0 && (
|
||||||
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
{activeTags.map((tag, i) => (
|
||||||
|
<span
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{tag.label}
|
||||||
|
<button onClick={tag.onRemove}>
|
||||||
|
<IoCloseOutline className="size-4" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
<div>
|
|
||||||
<div className="hidden" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default Filters5;
|
||||||
|
|||||||
@ -2,170 +2,199 @@ 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 { Button } from "@relume_io/relume-ui";
|
import { BiLogoLinkedinSquare } from "react-icons/bi";
|
||||||
import type { ButtonProps } from "@relume_io/relume-ui";
|
|
||||||
import { BiLogoDribbble, BiLogoLinkedinSquare } from "react-icons/bi";
|
|
||||||
import { FaXTwitter } from "react-icons/fa6";
|
|
||||||
|
|
||||||
type ImageProps = {
|
type ImageProps = {
|
||||||
src: string;
|
src: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Footer = {
|
|
||||||
heading: string;
|
|
||||||
description: string;
|
|
||||||
button: ButtonProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SocialLink = {
|
type SocialLink = {
|
||||||
href: string;
|
href: string;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TeamMember = {
|
type Talk = {
|
||||||
|
title: string;
|
||||||
|
type: "Talk" | "Workshop" | "Networking" | "AI and Future" | "Interaction";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TeamMember = {
|
||||||
image: ImageProps;
|
image: ImageProps;
|
||||||
name: string;
|
name: string;
|
||||||
jobTitle: string;
|
role: string;
|
||||||
|
company: string;
|
||||||
|
companyUrl?: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
category: string;
|
||||||
|
talks: Talk[];
|
||||||
socialLinks: SocialLink[];
|
socialLinks: SocialLink[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tagline: string;
|
|
||||||
heading: string;
|
|
||||||
description: string;
|
|
||||||
teamMembers: TeamMember[];
|
teamMembers: TeamMember[];
|
||||||
footerContent: Footer;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Team4Props = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;
|
export type Team4Props = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;
|
||||||
|
|
||||||
|
const talkBadgeClass = (type: Talk["type"]) => {
|
||||||
|
if (type === "Workshop") return "bg-acid-lime text-tech-navy";
|
||||||
|
if (type === "Networking") return "bg-acid-lime text-tech-navy";
|
||||||
|
return "bg-electric-violet text-cloud-white";
|
||||||
|
};
|
||||||
|
|
||||||
|
const MemberCard = ({ member }: { member: TeamMember }) => (
|
||||||
|
<div className="flex flex-col font-barlow">
|
||||||
|
<div className="relative mb-5 aspect-square w-full overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={member.image.src}
|
||||||
|
alt={member.image.alt}
|
||||||
|
className="absolute inset-0 size-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h5 className="text-md font-bold md:text-lg">{member.name}</h5>
|
||||||
|
<p className="text-sm text-gray-600">{member.role}</p>
|
||||||
|
<a
|
||||||
|
href={member.companyUrl ?? "#"}
|
||||||
|
className="mb-3 text-sm underline underline-offset-2 text-tech-navy"
|
||||||
|
>
|
||||||
|
{member.company}
|
||||||
|
</a>
|
||||||
|
<p className="mb-4 text-sm leading-relaxed text-gray-700">{member.description}</p>
|
||||||
|
<div className="mb-4 flex flex-wrap gap-2">
|
||||||
|
{member.talks.map((talk, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className={`rounded-full px-3 py-1 text-xs font-semibold ${talkBadgeClass(talk.type)}`}
|
||||||
|
>
|
||||||
|
{talk.title}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-auto flex gap-3">
|
||||||
|
{member.socialLinks.map((link, i) => (
|
||||||
|
<a key={i} href={link.href}>
|
||||||
|
{link.icon}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export const Team4 = (props: Team4Props) => {
|
export const Team4 = (props: Team4Props) => {
|
||||||
const { tagline, heading, description, teamMembers, footerContent } = {
|
const { teamMembers } = { ...Team4Defaults, ...props };
|
||||||
...Team4Defaults,
|
|
||||||
...props,
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<section id="relume" className="px-[5%] py-16 md:py-24 lg:py-28">
|
<section className="px-[5%] py-16 md:py-24 bg-cloud-white font-barlow">
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="rb-12 mb-12 max-w-lg md:mb-18 lg:mb-20">
|
{teamMembers.length === 0 ? (
|
||||||
<p className="mb-3 font-semibold md:mb-4">{tagline}</p>
|
<p className="py-16 text-center text-gray-400">Keine Speaker gefunden.</p>
|
||||||
<h2 className="rb-5 mb-5 text-5xl font-bold md:mb-6 md:text-7xl lg:text-8xl">
|
) : (
|
||||||
{heading}
|
<div className="grid grid-cols-1 gap-x-8 gap-y-12 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
</h2>
|
{teamMembers.map((member, i) => (
|
||||||
<p className="md:text-md">{description}</p>
|
<MemberCard key={i} member={member} />
|
||||||
</div>
|
))}
|
||||||
<div className="grid grid-cols-1 items-start justify-items-start gap-x-8 gap-y-12 md:grid-cols-2 md:gap-x-8 md:gap-y-16 lg:grid-cols-4">
|
|
||||||
{teamMembers.map((member, index) => (
|
|
||||||
<TeamMember key={index} member={member} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mt-14 w-full max-w-md md:mt-20 lg:mt-24">
|
|
||||||
<h4 className="mb-3 text-2xl font-bold md:mb-4 md:text-3xl md:leading-[1.3] lg:text-4xl">
|
|
||||||
{footerContent.heading}
|
|
||||||
</h4>
|
|
||||||
<p className="md:text-md">{footerContent.description}</p>
|
|
||||||
<div className="mt-6 flex flex-wrap gap-4 md:mt-8">
|
|
||||||
<Button {...footerContent.button}>{footerContent.button.title}</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TeamMember = ({ member }: { member: TeamMember }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="relative mb-5 aspect-square size-full overflow-hidden md:mb-6 md:pt-[100%]">
|
|
||||||
<img
|
|
||||||
src={member.image.src}
|
|
||||||
alt={member.image.alt}
|
|
||||||
className="absolute inset-0 size-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mb-3 md:mb-4">
|
|
||||||
<h5 className="text-md font-semibold md:text-lg">{member.name}</h5>
|
|
||||||
<h6 className="md:text-md">{member.jobTitle}</h6>
|
|
||||||
</div>
|
|
||||||
<p>{member.description}</p>
|
|
||||||
<div className="mt-6 grid grid-flow-col grid-cols-[max-content] gap-[0.875rem] self-start">
|
|
||||||
{member.socialLinks.map((link, index) => (
|
|
||||||
<a key={index} href={link.href}>
|
|
||||||
{link.icon}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Team4Defaults: Props = {
|
export const Team4Defaults: Props = {
|
||||||
tagline: "",
|
|
||||||
heading: "Unsere Speaker",
|
|
||||||
description: "",
|
|
||||||
teamMembers: [
|
teamMembers: [
|
||||||
{
|
{
|
||||||
image: { src: speaker1, alt: "Jens Riegelsberger" },
|
image: { src: speaker1, alt: "Jens Riegelsberger" },
|
||||||
name: "Jens Riegelsberger",
|
name: "Jens Riegelsberger",
|
||||||
jobTitle: "UX Director · Google",
|
role: "UX Director",
|
||||||
description: "Jens leitet die UX-Teams für Search und Maps sowie die globale UXR-Infrastruktur.",
|
company: "Google",
|
||||||
|
description:
|
||||||
|
"Jens leitet die UX-Teams für Search und Maps sowie die globale UXR-Infrastruktur. Der HCI-Experte lehrte als Gastprofessor an der UdK Berlin und bringt Erfahrung von Stationen bei Microsoft Research, Amazon und Apple mit.",
|
||||||
|
category: "Research",
|
||||||
|
talks: [
|
||||||
|
{ title: "Talk: Scaling Research", type: "Talk" },
|
||||||
|
{ title: "Workshop: Skalierbare Research-Infrastruktur", type: "Workshop" },
|
||||||
|
],
|
||||||
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
image: { src: speaker2, alt: "Marcus J. Low" },
|
image: { src: speaker2, alt: "Marcus J. Low" },
|
||||||
name: "Marcus J. Low",
|
name: "Marcus J. Low",
|
||||||
jobTitle: "Principal Designer · Airbnb",
|
role: "Principal Designer",
|
||||||
description: "Als Mitentwickler des ersten Airbnb Design Systems spricht Marcus über die Balance zwischen Markenästhetik und funktionaler Logik.",
|
company: "Airbnb",
|
||||||
|
description:
|
||||||
|
"Als Mitentwickler des ersten Airbnb Design Systems spricht Marcus über die Balance zwischen Markenästhetik und funktionaler Logik.",
|
||||||
|
category: "Design / UX",
|
||||||
|
talks: [{ title: "Workshop: The Future of Tokens", type: "Workshop" }],
|
||||||
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
image: { src: speaker3, alt: "Reto Gwerder" },
|
image: { src: speaker3, alt: "Reto Gwerder" },
|
||||||
name: "Reto Gwerder",
|
name: "Reto Gwerder",
|
||||||
jobTitle: "Head of Product · Ginetta",
|
role: "Head of Product",
|
||||||
description: "Reto steht für Schweizer Design-Qualität. Er analysiert, warum Simplizität oft die größte technische Herausforderung ist.",
|
company: "Ginetta",
|
||||||
|
description:
|
||||||
|
"Reto steht für Schweizer Design-Qualität. Er analysiert, warum Simplizität oft die größte technische Herausforderung ist.",
|
||||||
|
category: "Strategie",
|
||||||
|
talks: [{ title: "Talk: Simplify or Die", type: "Talk" }],
|
||||||
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
image: { src: speaker4, alt: "Dr. Elena Rossi" },
|
image: { src: speaker4, alt: "Dr. Elena Rossi" },
|
||||||
name: "Dr. Elena Rossi",
|
name: "Dr. Elena Rossi",
|
||||||
jobTitle: "Cognitive Psychologist · University of Milan",
|
role: "Cognitive Psychologist",
|
||||||
description: "Elena verbindet Wissenschaft mit Design. Sie erklärt, wie unser Gehirn auf Micro-Interactions reagiert.",
|
company: "University of Milan",
|
||||||
|
description:
|
||||||
|
"Elena verbindet Wissenschaft mit Design. Sie erklärt, wie unser Gehirn auf Micro-Interactions reagiert und wann wir uns manipuliert fühlen.",
|
||||||
|
category: "Psychologie",
|
||||||
|
talks: [{ title: "Deep Dive: Psychological UX", type: "Talk" }],
|
||||||
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
image: { src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg", alt: "Dr. Elena Rossi" },
|
image: { src: speaker1, alt: "Liam O'Connor" },
|
||||||
name: "Dr. Elena Rossi",
|
name: "Liam O'Connor",
|
||||||
jobTitle: "Cognitive Psychologist · University of Milan",
|
role: "Lead Product Designer",
|
||||||
description: "Elena verbindet Wissenschaft mit Design. Sie erklärt, wie unser Gehirn auf Micro-Interactions reagiert.",
|
company: "Spotify",
|
||||||
|
description:
|
||||||
|
"Liam gibt Einblicke, wie Spotify Personalisierung nutzt, ohne die Privatsphäre der Nutzer zu verletzen.",
|
||||||
|
category: "AI & Future",
|
||||||
|
talks: [{ title: "Talk: Trust in AI UX", type: "Talk" }],
|
||||||
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
image: { src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg", alt: "Dr. Elena Rossi" },
|
image: { src: speaker2, alt: "Fabienne Keller" },
|
||||||
name: "Dr. Elena Rossi",
|
name: "Fabienne Keller",
|
||||||
jobTitle: "Cognitive Psychologist · University of Milan",
|
role: "Gründerin",
|
||||||
description: "Elena verbindet Wissenschaft mit Design. Sie erklärt, wie unser Gehirn auf Micro-Interactions reagiert.",
|
company: "User-First Agency",
|
||||||
|
description:
|
||||||
|
"Fabienne ist Expertin für User Research in komplexen B2B-Umfeldern und wie man Stakeholder von Test-Ergebnissen überzeugt.",
|
||||||
|
category: "Research",
|
||||||
|
talks: [{ title: "Workshop: Research Repositories", type: "Workshop" }],
|
||||||
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
image: { src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg", alt: "Dr. Elena Rossi" },
|
image: { src: speaker3, alt: "Thomas Meyer" },
|
||||||
name: "Dr. Elena Rossi",
|
name: 'Thomas "Tom" Meyer',
|
||||||
jobTitle: "Cognitive Psychologist · University of Milan",
|
role: "Creative Director",
|
||||||
description: "Elena verbindet Wissenschaft mit Design. Sie erklärt, wie unser Gehirn auf Micro-Interactions reagiert.",
|
company: "Swisscom",
|
||||||
|
description:
|
||||||
|
"Tom zeigt, wie man in großen Konzernen eine Design-Kultur etabliert, die über das visuelle Layer hinausgeht.",
|
||||||
|
category: "Strategie",
|
||||||
|
talks: [{ title: "Networking: Leading Teams", type: "Networking" }],
|
||||||
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
image: { src: "https://d22po4pjz3o32e.cloudfront.net/placeholder-image.svg", alt: "Dr. Elena Rossi" },
|
image: { src: speaker4, alt: "Sarah M. Widmer" },
|
||||||
name: "Dr. Elena Rossi",
|
name: "Sarah M. Widmer",
|
||||||
jobTitle: "Cognitive Psychologist · University of Milan",
|
role: "Senior UX Architect",
|
||||||
description: "Elena verbindet Wissenschaft mit Design. Sie erklärt, wie unser Gehirn auf Micro-Interactions reagiert.",
|
company: "SBB",
|
||||||
|
description:
|
||||||
|
"Sarah gestaltet die Mobilität von morgen. Sie zeigt, wie man Millionen von Nutzern barrierefrei durch den digitalen öV-Dschungel leitet.",
|
||||||
|
category: "Design / UX",
|
||||||
|
talks: [{ title: "Talk: Inclusive Design at Scale", type: "Talk" }],
|
||||||
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
socialLinks: [{ href: "#", icon: <BiLogoLinkedinSquare className="size-6" /> }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
footerContent: {
|
|
||||||
heading: "",
|
|
||||||
description: "",
|
|
||||||
button: { title: "", variant: "secondary" },
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default Team4;
|
||||||
|
|||||||
@ -1,21 +1,92 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
import { Navbar3 } from "../components/Navbar";
|
import { Navbar3 } from "../components/Navbar";
|
||||||
import { Header23 } from "../components/Header23";
|
import { Header23 } from "../components/Header23";
|
||||||
import { Filters5 } from "../components/Filters5";
|
import { Filters5 } from "../components/Filters5";
|
||||||
import { Event33 } from "../components/Event33";
|
import { Event33 } from "../components/Event33";
|
||||||
|
import type { ProgramEvent } from "../components/Event33";
|
||||||
import { Banner15 } from "../components/Banner15";
|
import { Banner15 } from "../components/Banner15";
|
||||||
import { Footer3 } from "../components/Footer";
|
import { Footer3 } from "../components/Footer";
|
||||||
|
|
||||||
|
import spk1 from "../assets/speakers/speaker1.jpg";
|
||||||
|
import spk2 from "../assets/speakers/speaker2.jpg";
|
||||||
|
import spk3 from "../assets/speakers/speaker3.jpg";
|
||||||
|
import spk4 from "../assets/speakers/speaker4.jpg";
|
||||||
|
|
||||||
|
const ALL_EVENTS: ProgramEvent[] = [
|
||||||
|
// Donnerstag
|
||||||
|
{ id: "do-1", time: "10:00 – 11:30 Uhr", image: { src: spk1, alt: "Jens Riegelsberger" }, title: "Opening Keynote: The Future of UX", speaker: "Jens Riegelsberger", type: "Talk", level: "Senior", day: "Donnerstag", speakerUrl: "/speaker" },
|
||||||
|
{ id: "do-2", time: "12:00 – 13:00 Uhr", image: { src: spk2, alt: "Marcus J. Low" }, title: "Workshop: Design Systems at Scale", speaker: "Marcus J. Low", type: "Workshop", level: "Mid-Level", day: "Donnerstag", speakerUrl: "/speaker" },
|
||||||
|
{ id: "do-3", time: "14:00 – 15:30 Uhr", image: { src: spk3, alt: "Reto Gwerder" }, title: "Interaction: Prototyping Fast", speaker: "Reto Gwerder", type: "Interaction", level: "Junior", day: "Donnerstag", speakerUrl: "/speaker" },
|
||||||
|
{ id: "do-4", time: "16:00 – 17:30 Uhr", image: { src: spk4, alt: "Dr. Elena Rossi" }, title: "AI and Future: Intelligence in Design", speaker: "Dr. Elena Rossi", type: "AI and Future", level: "Senior", day: "Donnerstag", speakerUrl: "/speaker" },
|
||||||
|
{ id: "do-5", time: "18:00 – 19:30 Uhr", image: { src: spk1, alt: "Liam O'Connor" }, title: "Talk: Ethical AI", speaker: "Liam O'Connor", type: "Talk", level: "Mid-Level", day: "Donnerstag", speakerUrl: "/speaker" },
|
||||||
|
// Freitag
|
||||||
|
{ id: "fr-1", time: "12:00 – 13:30 Uhr", image: { src: spk1, alt: "Jens Riegelsberger" }, title: "Scaling Research", speaker: "Jens Riegelsberger", type: "Talk", level: "Senior", day: "Freitag", speakerUrl: "/speaker" },
|
||||||
|
{ id: "fr-2", time: "12:00 – 13:00 Uhr", image: { src: spk2, alt: "Marcus J. Low" }, title: "Workshop: The Future of Tokens", speaker: "Marcus J. Low", type: "Workshop", level: "Mid-Level", day: "Freitag", speakerUrl: "/speaker" },
|
||||||
|
{ id: "fr-3", time: "14:00 – 15:30 Uhr", image: { src: spk3, alt: "Reto Gwerder" }, title: "Talk: Simplify or Die", speaker: "Reto Gwerder", type: "Talk", level: "Junior", day: "Freitag", speakerUrl: "/speaker" },
|
||||||
|
{ id: "fr-4", time: "16:00 – 17:30 Uhr", image: { src: spk4, alt: "Dr. Elena Rossi" }, title: "Deep Dive: Psychological UX", speaker: "Dr. Elena Rossi", type: "Talk", level: "Senior", day: "Freitag", speakerUrl: "/speaker" },
|
||||||
|
{ id: "fr-5", time: "18:00 – 19:30 Uhr", image: { src: spk1, alt: "Liam O'Connor" }, title: "Talk: Trust in AI", speaker: "Liam O'Connor", type: "Talk", level: "Mid-Level", day: "Freitag", speakerUrl: "/speaker" },
|
||||||
|
{ id: "fr-6", time: "20:00 – 21:30 Uhr", image: { src: spk2, alt: "Fabienne Keller" }, title: "Workshop: Research Repositories", speaker: "Fabienne Keller", type: "Workshop", level: "Senior", day: "Freitag", speakerUrl: "/speaker" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TYPE_TABS = ["Talk", "Workshop", "Interaction", "AI and Future"];
|
||||||
|
const LEVEL_TABS: Record<string, string> = {
|
||||||
|
"Senior": "Senior",
|
||||||
|
"Mid-Level": "Mid-Level",
|
||||||
|
"Junior": "Junior",
|
||||||
|
};
|
||||||
|
|
||||||
const Programm = () => {
|
const Programm = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState("all");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [activeDay, setActiveDay] = useState("all");
|
||||||
|
|
||||||
|
const filteredEvents = useMemo(() => {
|
||||||
|
return ALL_EVENTS.filter((e) => {
|
||||||
|
if (activeTab !== "all") {
|
||||||
|
const matchesType = TYPE_TABS.includes(activeTab) && e.type === activeTab;
|
||||||
|
const matchesLevel = LEVEL_TABS[activeTab] && e.level === LEVEL_TABS[activeTab];
|
||||||
|
if (!matchesType && !matchesLevel) return false;
|
||||||
|
}
|
||||||
|
if (searchQuery) {
|
||||||
|
const q = searchQuery.toLowerCase();
|
||||||
|
if (!e.title.toLowerCase().includes(q) && !e.speaker.toLowerCase().includes(q)) return false;
|
||||||
|
}
|
||||||
|
if (activeDay !== "all" && e.day !== activeDay) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [activeTab, searchQuery, activeDay]);
|
||||||
|
|
||||||
|
const activeTags = [
|
||||||
|
searchQuery ? { label: searchQuery, onRemove: () => setSearchQuery("") } : null,
|
||||||
|
activeDay !== "all" ? { label: activeDay, onRemove: () => setActiveDay("all") } : null,
|
||||||
|
activeTab !== "all" ? { label: activeTab, onRemove: () => setActiveTab("all") } : null,
|
||||||
|
].filter(Boolean) as { label: string; onRemove: () => void }[];
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setActiveTab("all");
|
||||||
|
setSearchQuery("");
|
||||||
|
setActiveDay("all");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Navbar3 />
|
<Navbar3 />
|
||||||
<Header23 />
|
<Header23 />
|
||||||
<Filters5 />
|
<Filters5
|
||||||
<Event33 />
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
onSearchChange={setSearchQuery}
|
||||||
|
activeDay={activeDay}
|
||||||
|
onDayChange={setActiveDay}
|
||||||
|
activeTags={activeTags}
|
||||||
|
onReset={handleReset}
|
||||||
|
/>
|
||||||
|
<Event33 events={filteredEvents} />
|
||||||
<Banner15 />
|
<Banner15 />
|
||||||
<Footer3 />
|
<Footer3 />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Programm;
|
export default Programm;
|
||||||
|
|||||||
@ -1,26 +1,76 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
import { Navbar3 } from "../components/Navbar";
|
import { Navbar3 } from "../components/Navbar";
|
||||||
import { Header23 } from "../components/Header23";
|
import { Header23 } from "../components/Header23";
|
||||||
import { Filters5 } from "../components/Filters5";
|
import { Filters5 } from "../components/Filters5";
|
||||||
import { Team4 } from "../components/Team4";
|
import { Team4 } from "../components/Team4";
|
||||||
|
import { Team4Defaults } from "../components/Team4";
|
||||||
import { Footer3 } from "../components/Footer";
|
import { Footer3 } from "../components/Footer";
|
||||||
|
|
||||||
const Programm = () => {
|
const SPEAKER_TABS = [
|
||||||
|
{ value: "all", label: "Alle ansehen" },
|
||||||
|
{ value: "Strategie", label: "Strategie" },
|
||||||
|
{ value: "Research", label: "Research" },
|
||||||
|
{ value: "Design / UX", label: "Design / UX" },
|
||||||
|
{ value: "AI & Future", label: "AI & Future" },
|
||||||
|
{ value: "Psychologie", label: "Psychologie" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ALL_SPEAKERS = Team4Defaults.teamMembers;
|
||||||
|
|
||||||
|
const Speaker = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState("all");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
const filteredSpeakers = useMemo(() => {
|
||||||
|
return ALL_SPEAKERS.filter((s) => {
|
||||||
|
if (activeTab !== "all" && s.category !== activeTab) return false;
|
||||||
|
if (searchQuery) {
|
||||||
|
const q = searchQuery.toLowerCase();
|
||||||
|
if (
|
||||||
|
!s.name.toLowerCase().includes(q) &&
|
||||||
|
!s.role.toLowerCase().includes(q) &&
|
||||||
|
!s.company.toLowerCase().includes(q)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [activeTab, searchQuery]);
|
||||||
|
|
||||||
|
const activeTags = [
|
||||||
|
searchQuery ? { label: searchQuery, onRemove: () => setSearchQuery("") } : null,
|
||||||
|
activeTab !== "all" ? { label: activeTab, onRemove: () => setActiveTab("all") } : null,
|
||||||
|
].filter(Boolean) as { label: string; onRemove: () => void }[];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Navbar3 />
|
<Navbar3 />
|
||||||
<Header23
|
<Header23
|
||||||
heading="Unsere Speaker"
|
heading="Unsere Speaker"
|
||||||
description="Erfahre von den klügsten Köpfen der Branche, wie sie komplexe Design-Herausforderungen meistern."
|
description="Erfahre von den klügsten Köpfen der Branche, wie sie komplexe Design-Herausforderungen meistern. Von Schweizer Präzision im Interface bis hin zu globalen Strategien führender Tech-Giganten – unsere Speaker bringen handfeste Insights statt nur Buzzwords."
|
||||||
buttons={[
|
buttons={[
|
||||||
{ title: "Zu den Speakern", variant: "primary" },
|
{ title: "Zu den Speakern", variant: "primary" },
|
||||||
{ title: "Zum Programm", variant: "secondary" },
|
{ title: "Zum Programm", variant: "secondary" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Filters5 />
|
<Filters5
|
||||||
<Team4 />
|
tabs={SPEAKER_TABS}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
onSearchChange={setSearchQuery}
|
||||||
|
searchLabel="Stichwortsuche"
|
||||||
|
resetLabel="Löschen"
|
||||||
|
activeTags={activeTags}
|
||||||
|
onReset={() => { setSearchQuery(""); setActiveTab("all"); }}
|
||||||
|
useFilterToggle={false}
|
||||||
|
showDayFilter={false}
|
||||||
|
showSortButton={false}
|
||||||
|
/>
|
||||||
|
<Team4 teamMembers={filteredSpeakers} />
|
||||||
<Footer3 />
|
<Footer3 />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Programm;
|
export default Speaker;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user