adjustments based on the coaching feedback
This commit is contained in:
parent
ed0d261056
commit
28803b266a
1
relume-test/public/_redirects
Normal file
1
relume-test/public/_redirects
Normal file
@ -0,0 +1 @@
|
||||
/* /index.html 200
|
||||
1
relume-test/src/assets/logo-adobe.svg
Normal file
1
relume-test/src/assets/logo-adobe.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="m13.966 22.624l-1.69-4.281H8.122l3.892-9.144l5.662 13.425zM8.884 1.376H0v21.248zm15.116 0h-8.884L24 22.624Z"/></svg>
|
||||
|
After Width: | Height: | Size: 230 B |
1
relume-test/src/assets/logo-figma.svg
Normal file
1
relume-test/src/assets/logo-figma.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M15.852 8.981h-4.588V0h4.588c2.476 0 4.49 2.014 4.49 4.49s-2.014 4.491-4.49 4.491M12.735 7.51h3.117c1.665 0 3.019-1.355 3.019-3.019s-1.355-3.019-3.019-3.019h-3.117zm0 1.471H8.148c-2.476 0-4.49-2.014-4.49-4.49S5.672 0 8.148 0h4.588v8.981zm-4.587-7.51c-1.665 0-3.019 1.355-3.019 3.019s1.354 3.02 3.019 3.02h3.117V1.471zm4.587 15.019H8.148c-2.476 0-4.49-2.014-4.49-4.49s2.014-4.49 4.49-4.49h4.588v8.98zM8.148 8.981c-1.665 0-3.019 1.355-3.019 3.019s1.355 3.019 3.019 3.019h3.117V8.981zM8.172 24c-2.489 0-4.515-2.014-4.515-4.49s2.014-4.49 4.49-4.49h4.588v4.441c0 2.503-2.047 4.539-4.563 4.539m-.024-7.51a3.023 3.023 0 0 0-3.019 3.019c0 1.665 1.365 3.019 3.044 3.019c1.705 0 3.093-1.376 3.093-3.068v-2.97zm7.704 0h-.098c-2.476 0-4.49-2.014-4.49-4.49s2.014-4.49 4.49-4.49h.098c2.476 0 4.49 2.014 4.49 4.49s-2.014 4.49-4.49 4.49m-.097-7.509c-1.665 0-3.019 1.355-3.019 3.019s1.355 3.019 3.019 3.019h.098c1.665 0 3.019-1.355 3.019-3.019s-1.355-3.019-3.019-3.019z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
relume-test/src/assets/logo-google.svg
Normal file
1
relume-test/src/assets/logo-google.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133c-1.147 1.147-2.933 2.4-6.053 2.4c-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0C5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36c2.16-2.16 2.84-5.213 2.84-7.667c0-.76-.053-1.467-.173-2.053z"/></svg>
|
||||
|
After Width: | Height: | Size: 466 B |
@ -1,13 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
type ImageProps = {
|
||||
src: string;
|
||||
alt?: string;
|
||||
};
|
||||
import logoFigma from "../assets/logo-figma.svg";
|
||||
import logoAdobe from "../assets/logo-adobe.svg";
|
||||
import logoGoogle from "../assets/logo-google.svg";
|
||||
|
||||
type Section = {
|
||||
title: string;
|
||||
image: ImageProps;
|
||||
image?: { src: string; alt: string };
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@ -17,12 +15,9 @@ type Props = {
|
||||
export type Banner15Props = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;
|
||||
|
||||
export const Banner15 = (props: Banner15Props) => {
|
||||
const { sections } = {
|
||||
...Banner15Defaults,
|
||||
...props,
|
||||
};
|
||||
const { sections } = { ...Banner15Defaults, ...props };
|
||||
return (
|
||||
<section id="relume" className="flex w-screen max-w-full justify-end overflow-hidden bg-cloud-white">
|
||||
<section className="flex w-screen max-w-full justify-end overflow-hidden bg-cloud-white">
|
||||
<div className="flex justify-end">
|
||||
{Array(2)
|
||||
.fill(0)
|
||||
@ -31,18 +26,20 @@ export const Banner15 = (props: Banner15Props) => {
|
||||
key={index}
|
||||
className="grid animate-marquee-right auto-cols-max grid-flow-col grid-cols-[max-content] items-center justify-around py-4"
|
||||
>
|
||||
{sections.map((section, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{sections.map((section, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<div className="flex items-center justify-center whitespace-nowrap px-8 text-center lg:text-left">
|
||||
<h1 className="text-6xl font-bold md:text-9xl lg:text-10xl">{section.title}</h1>
|
||||
</div>
|
||||
<div className="relative aspect-[3/2] w-full overflow-hidden object-cover">
|
||||
<img
|
||||
src={section.image.src}
|
||||
alt={section.image.alt}
|
||||
className="aspect-[3/2] size-full h-16 max-h-24 object-cover md:h-auto"
|
||||
/>
|
||||
</div>
|
||||
{section.image && (
|
||||
<div className="relative flex items-center justify-center px-4">
|
||||
<img
|
||||
src={section.image.src}
|
||||
alt={section.image.alt}
|
||||
className="h-16 max-h-24 w-auto object-contain md:h-20"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
@ -55,32 +52,21 @@ export const Banner15 = (props: Banner15Props) => {
|
||||
export const Banner15Defaults: Props = {
|
||||
sections: [
|
||||
{
|
||||
title: "UX Schweiz",
|
||||
image: {
|
||||
src: "https://relume-assets.s3.us-east-1.amazonaws.com/placeholder-image.svg",
|
||||
alt: "UX Schweiz",
|
||||
},
|
||||
title: "Figma",
|
||||
image: { src: logoFigma, alt: "Figma" },
|
||||
},
|
||||
{
|
||||
title: "Adobe",
|
||||
image: {
|
||||
src: "https://relume-assets.s3.us-east-1.amazonaws.com/placeholder-image.svg",
|
||||
alt: "Adobe",
|
||||
},
|
||||
image: { src: logoAdobe, alt: "Adobe" },
|
||||
},
|
||||
{
|
||||
title: "Google",
|
||||
image: {
|
||||
src: "https://relume-assets.s3.us-east-1.amazonaws.com/placeholder-image.svg",
|
||||
alt: "Google",
|
||||
},
|
||||
image: { src: logoGoogle, alt: "Google" },
|
||||
},
|
||||
{
|
||||
title: "Figma",
|
||||
image: {
|
||||
src: "https://relume-assets.s3.us-east-1.amazonaws.com/placeholder-image.svg",
|
||||
alt: "Figma",
|
||||
},
|
||||
title: "UX Schweiz",
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export default Banner15;
|
||||
|
||||
@ -29,8 +29,11 @@ const levelStyles: Record<EventLevel, string> = {
|
||||
};
|
||||
|
||||
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] md:gap-6 md:py-7">
|
||||
<span className="text-sm font-semibold text-tech-navy">{event.time}</span>
|
||||
<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-[8rem_5.5rem_1fr_max-content] md:gap-6 md:py-7">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-xs font-bold uppercase tracking-wide text-electric-violet">{event.day}</span>
|
||||
<span className="text-sm font-semibold text-tech-navy">{event.time}</span>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
@ -52,13 +55,6 @@ const EventRow = ({ event }: { event: ProgramEvent }) => (
|
||||
>
|
||||
Ü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>
|
||||
);
|
||||
|
||||
@ -75,6 +71,14 @@ export const Event33 = ({ events }: Props) => {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-10 flex justify-center">
|
||||
<a
|
||||
href="/tickets"
|
||||
className="rounded-2xl bg-electric-violet px-8 py-3 text-base font-normal text-cloud-white hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Jetzt Ticket sichern
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@ -1,31 +1,39 @@
|
||||
import { useState } from "react";
|
||||
import { BiFilter, BiSearch } from "react-icons/bi";
|
||||
import { IoCloseOutline } from "react-icons/io5";
|
||||
import { RxChevronDown } from "react-icons/rx";
|
||||
import { useState } from "react";
|
||||
|
||||
export type FilterTab = { value: string; label: string };
|
||||
type ActiveTag = { label: string; onRemove: () => void };
|
||||
|
||||
type Props = {
|
||||
// Speaker page (single-select)
|
||||
tabs?: FilterTab[];
|
||||
activeTab?: string;
|
||||
onTabChange?: (tab: string) => void;
|
||||
// Programm page (multi-select)
|
||||
activeTabs?: string[];
|
||||
onTabsChange?: (tabs: string[]) => void;
|
||||
// Shared
|
||||
searchQuery?: string;
|
||||
onSearchChange?: (q: string) => void;
|
||||
searchLabel?: string;
|
||||
resetLabel?: string;
|
||||
activeTags?: ActiveTag[];
|
||||
onReset?: () => void;
|
||||
// Programm-only
|
||||
useFilterToggle?: boolean;
|
||||
showDayFilter?: boolean;
|
||||
showSortButton?: boolean;
|
||||
sortOrder?: "asc" | "desc";
|
||||
onSortChange?: (order: "asc" | "desc") => void;
|
||||
// Legacy day props (kept for backwards compat)
|
||||
activeDay?: string;
|
||||
onDayChange?: (day: string) => void;
|
||||
};
|
||||
|
||||
const PROGRAMM_TABS: FilterTab[] = [
|
||||
{ value: "all", label: "Alle Anzeigen" },
|
||||
{ value: "Donnerstag", label: "Donnerstag" },
|
||||
{ value: "Freitag", label: "Freitag" },
|
||||
{ value: "Talk", label: "Talk" },
|
||||
{ value: "Workshop", label: "Workshop" },
|
||||
{ value: "Interaction", label: "Interaction" },
|
||||
@ -35,16 +43,12 @@ const PROGRAMM_TABS: FilterTab[] = [
|
||||
{ value: "Junior", label: "Junior-Level" },
|
||||
];
|
||||
|
||||
const DAYS = [
|
||||
{ value: "all", label: "Alle Tage" },
|
||||
{ value: "Donnerstag", label: "Donnerstag" },
|
||||
{ value: "Freitag", label: "Freitag" },
|
||||
];
|
||||
|
||||
export const Filters5 = ({
|
||||
tabs = PROGRAMM_TABS,
|
||||
tabs,
|
||||
activeTab = "all",
|
||||
onTabChange = () => {},
|
||||
activeTabs = [],
|
||||
onTabsChange = () => {},
|
||||
searchQuery = "",
|
||||
onSearchChange = () => {},
|
||||
searchLabel = "Stichwortsuche",
|
||||
@ -52,40 +56,51 @@ export const Filters5 = ({
|
||||
activeTags = [],
|
||||
onReset = () => {},
|
||||
useFilterToggle = true,
|
||||
showDayFilter = true,
|
||||
showSortButton = true,
|
||||
activeDay = "all",
|
||||
onDayChange = () => {},
|
||||
sortOrder = "asc",
|
||||
onSortChange = () => {},
|
||||
}: Props) => {
|
||||
const [sortOpen, setSortOpen] = useState(false);
|
||||
const [dayOpen, setDayOpen] = useState(false);
|
||||
|
||||
const selectedDayLabel = DAYS.find((d) => d.value === activeDay)?.label ?? "Alle Tage";
|
||||
const toggleTab = (value: string) => {
|
||||
if (activeTabs.includes(value)) {
|
||||
onTabsChange(activeTabs.filter((t) => t !== value));
|
||||
} else {
|
||||
onTabsChange([...activeTabs, value]);
|
||||
}
|
||||
};
|
||||
|
||||
const TabPills = () => (
|
||||
<div className="flex items-center gap-1 overflow-x-auto no-scrollbar">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
onClick={() => onTabChange(tab.value)}
|
||||
className={`shrink-0 rounded-2xl px-4 py-2 text-base font-semibold transition-colors ${
|
||||
activeTab === tab.value
|
||||
? "bg-electric-violet text-cloud-white"
|
||||
: "text-tech-navy hover:text-electric-violet"
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Speaker page: just tabs + search, no day filter or sort
|
||||
// Speaker page: multi-select tabs + search
|
||||
if (!useFilterToggle) {
|
||||
const speakerTabs = tabs ?? [];
|
||||
return (
|
||||
<section className="bg-cloud-white font-barlow border-b border-gray-200 px-[5%] py-8">
|
||||
<div className="container flex flex-col gap-6">
|
||||
<TabPills />
|
||||
<div className="flex items-center gap-1 overflow-x-auto no-scrollbar">
|
||||
<button
|
||||
onClick={() => onTabsChange([])}
|
||||
className={`shrink-0 rounded-2xl px-4 py-2 text-base font-semibold transition-colors ${
|
||||
activeTabs.length === 0
|
||||
? "bg-electric-violet text-cloud-white"
|
||||
: "text-tech-navy hover:text-electric-violet"
|
||||
}`}
|
||||
>
|
||||
Alle ansehen
|
||||
</button>
|
||||
{speakerTabs.map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
onClick={() => toggleTab(tab.value)}
|
||||
className={`shrink-0 rounded-2xl px-4 py-2 text-base font-semibold transition-colors ${
|
||||
activeTabs.includes(tab.value)
|
||||
? "bg-electric-violet text-cloud-white"
|
||||
: "text-tech-navy hover:text-electric-violet"
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<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>
|
||||
@ -93,17 +108,17 @@ export const Filters5 = ({
|
||||
{resetLabel}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex h-12 items-center gap-3 rounded-2xl border border-tech-navy px-3">
|
||||
<BiSearch className="size-6 shrink-0 text-tech-navy" />
|
||||
<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
|
||||
className="flex-1 bg-transparent text-base text-tech-navy outline-none placeholder:text-tech-navy/40"
|
||||
className="flex-1 bg-transparent text-base text-electric-violet outline-none placeholder:text-electric-violet/50"
|
||||
placeholder="Suchen..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button onClick={() => onSearchChange("")}>
|
||||
<IoCloseOutline className="size-5 text-tech-navy" />
|
||||
<IoCloseOutline className="size-5 text-electric-violet" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@ -128,20 +143,43 @@ export const Filters5 = ({
|
||||
);
|
||||
}
|
||||
|
||||
// Programm page: full filter bar, always visible
|
||||
// Programm page: multi-select filter bar
|
||||
return (
|
||||
<section className="bg-cloud-white font-barlow px-[5%] py-8">
|
||||
<div className="container flex flex-col gap-8">
|
||||
|
||||
{/* Row 1: Filter label + category pills + sort */}
|
||||
{/* Row 1: Filter label + multi-select pills + sort */}
|
||||
<div className="flex items-center gap-4">
|
||||
<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">
|
||||
<div className="flex shrink-0 items-center gap-2 text-base font-semibold text-tech-navy">
|
||||
<BiFilter className="size-6" />
|
||||
Filter
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center overflow-x-auto no-scrollbar">
|
||||
<TabPills />
|
||||
<div className="flex flex-1 items-center gap-1 overflow-x-auto no-scrollbar">
|
||||
{/* "Alle" pill — active only when nothing selected */}
|
||||
<button
|
||||
onClick={() => onTabsChange([])}
|
||||
className={`shrink-0 rounded-2xl px-4 py-2 text-base font-semibold transition-colors ${
|
||||
activeTabs.length === 0
|
||||
? "bg-electric-violet text-cloud-white"
|
||||
: "text-tech-navy hover:text-electric-violet"
|
||||
}`}
|
||||
>
|
||||
Alle
|
||||
</button>
|
||||
{PROGRAMM_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
onClick={() => toggleTab(tab.value)}
|
||||
className={`shrink-0 rounded-2xl px-4 py-2 text-base font-semibold transition-colors ${
|
||||
activeTabs.includes(tab.value)
|
||||
? "bg-electric-violet text-cloud-white"
|
||||
: "text-tech-navy hover:text-electric-violet"
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showSortButton && (
|
||||
@ -157,85 +195,48 @@ export const Filters5 = ({
|
||||
</button>
|
||||
{sortOpen && (
|
||||
<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) => (
|
||||
<button
|
||||
key={opt}
|
||||
className="block w-full px-4 py-2.5 text-left text-sm text-tech-navy hover:bg-electric-violet/10"
|
||||
onClick={() => setSortOpen(false)}
|
||||
>
|
||||
{opt}
|
||||
</button>
|
||||
))}
|
||||
{(["asc", "desc"] as const).map((order) => {
|
||||
const label = order === "asc" ? "Zeit aufsteigend" : "Zeit absteigend";
|
||||
return (
|
||||
<button
|
||||
key={order}
|
||||
className={`block w-full px-4 py-2.5 text-left text-sm transition-colors hover:bg-electric-violet/10 ${
|
||||
sortOrder === order ? "font-semibold text-electric-violet" : "text-tech-navy"
|
||||
}`}
|
||||
onClick={() => { onSortChange(order); setSortOpen(false); }}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 2: Search + Day filter — always visible */}
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||
|
||||
{/* Search */}
|
||||
<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}
|
||||
</button>
|
||||
</div>
|
||||
<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
|
||||
className="flex-1 bg-transparent text-base text-electric-violet outline-none placeholder:text-electric-violet/50"
|
||||
placeholder="Titel oder Speaker..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button onClick={() => onSearchChange("")}>
|
||||
<IoCloseOutline className="size-5 text-electric-violet" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* Row 2: Full-width search */}
|
||||
<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}
|
||||
</button>
|
||||
</div>
|
||||
<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
|
||||
className="flex-1 bg-transparent text-base text-electric-violet outline-none placeholder:text-electric-violet/50"
|
||||
placeholder="Titel oder Speaker..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button onClick={() => onSearchChange("")}>
|
||||
<IoCloseOutline className="size-5 text-electric-violet" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Day dropdown */}
|
||||
{showDayFilter && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-base font-medium text-tech-navy">Filtern nach Eventtage</span>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setDayOpen((v) => !v)}
|
||||
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"
|
||||
>
|
||||
<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) => (
|
||||
<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}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Active tags */}
|
||||
|
||||
@ -1,43 +1,114 @@
|
||||
import headerImage from "../assets/Header.png";
|
||||
import { Button } from "@relume_io/relume-ui";
|
||||
import type { ButtonProps } from "@relume_io/relume-ui";
|
||||
import logoAdobe from "../assets/logo-adobe.svg";
|
||||
import logoFigma from "../assets/logo-figma.svg";
|
||||
import logoGoogle from "../assets/logo-google.svg";
|
||||
|
||||
type ImageProps = {
|
||||
src: string;
|
||||
alt?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
heading: string;
|
||||
description: string;
|
||||
buttons: ButtonProps[];
|
||||
image: ImageProps;
|
||||
};
|
||||
|
||||
export type Header1Props = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;
|
||||
export type Header1Props = React.ComponentPropsWithoutRef<"section">;
|
||||
|
||||
export const Header1 = (props: Header1Props) => {
|
||||
const { heading, description, buttons, image } = {
|
||||
...Header1Defaults,
|
||||
...props,
|
||||
};
|
||||
return (
|
||||
<section id="relume" className="px-[5%] py-16 md:py-24 lg:py-28 bg-tech-navy font-barlow">
|
||||
<div className="container">
|
||||
<div className="grid grid-cols-1 gap-x-20 gap-y-12 md:gap-y-16 lg:grid-cols-2 lg:items-center">
|
||||
<div>
|
||||
<h1 className="mb-5 text-6xl font-bold md:mb-6 md:text-9xl lg:text-10xl text-acid-lime whitespace-pre-wrap">{heading}</h1>
|
||||
<p className="text-[48px] font-bold leading-[1.2] text-cloud-white whitespace-pre-wrap">{description}</p>
|
||||
<div className="mt-6 flex flex-wrap gap-4 md:mt-8">
|
||||
{buttons.map((button, index) => (
|
||||
<Button key={index} {...button}>
|
||||
{button.title}
|
||||
</Button>
|
||||
<section
|
||||
className="relative flex min-h-[92vh] flex-col overflow-hidden bg-tech-navy font-barlow"
|
||||
{...props}
|
||||
>
|
||||
{/* Mobile-only faint background image */}
|
||||
<div className="absolute inset-0 lg:hidden pointer-events-none">
|
||||
<img src={headerImage} alt="" className="h-full w-full object-cover opacity-10" />
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-tech-navy/60 to-tech-navy" />
|
||||
</div>
|
||||
|
||||
{/* Desktop right-side image panel */}
|
||||
<div className="absolute right-0 top-0 hidden h-full w-[52%] lg:block pointer-events-none">
|
||||
<img
|
||||
src={headerImage}
|
||||
alt=""
|
||||
className="h-full w-full object-cover opacity-90"
|
||||
/>
|
||||
{/* Blend edge left */}
|
||||
<div className="absolute inset-y-0 left-0 w-56 bg-gradient-to-r from-tech-navy to-transparent" />
|
||||
{/* Top vignette */}
|
||||
<div className="absolute inset-x-0 top-0 h-40 bg-gradient-to-b from-tech-navy to-transparent" />
|
||||
{/* Bottom vignette */}
|
||||
<div className="absolute inset-x-0 bottom-0 h-28 bg-gradient-to-t from-tech-navy to-transparent" />
|
||||
</div>
|
||||
|
||||
{/* Glow blobs */}
|
||||
<div className="absolute -left-24 top-1/4 h-[480px] w-[480px] rounded-full bg-electric-violet/20 blur-3xl pointer-events-none" />
|
||||
<div className="absolute left-1/3 top-0 h-[280px] w-[280px] rounded-full bg-electric-violet/10 blur-3xl pointer-events-none" />
|
||||
<div className="absolute bottom-0 left-0 h-[220px] w-[460px] rounded-full bg-acid-lime/5 blur-3xl pointer-events-none" />
|
||||
|
||||
{/* Main content */}
|
||||
<div className="relative z-10 flex flex-1 items-center px-[5%] py-24 md:py-32">
|
||||
<div className="container">
|
||||
<div className="mx-auto max-w-xl text-center lg:mx-0 lg:text-left">
|
||||
|
||||
{/* Date badge */}
|
||||
<div className="mb-8 flex items-center justify-center gap-3 lg:justify-start">
|
||||
<div className="h-px w-10 bg-acid-lime/60" />
|
||||
<span className="text-xs font-semibold uppercase tracking-[0.2em] text-acid-lime">
|
||||
25. – 26. Juni 2026 · Zürich
|
||||
</span>
|
||||
<div className="h-px w-10 bg-acid-lime/60" />
|
||||
</div>
|
||||
|
||||
{/* Heading */}
|
||||
<h1 className="mb-3 text-[80px] font-bold leading-none tracking-tight text-acid-lime md:text-[110px] lg:text-[130px]">
|
||||
UXplore
|
||||
</h1>
|
||||
<p className="mb-6 text-3xl font-bold text-cloud-white md:text-4xl">
|
||||
Konferenz 2026
|
||||
</p>
|
||||
|
||||
{/* Tagline */}
|
||||
<p className="mb-10 max-w-md text-base leading-relaxed text-cloud-white/60 md:text-lg lg:mx-0 mx-auto">
|
||||
Zwei Tage, zwölf Sessions, ein Ziel: UX, das wirklich wirkt. Die führende Schweizer Konferenz für Designer, Produktteams und alle, die nutzerzentriertes Denken in die Praxis bringen.
|
||||
</p>
|
||||
|
||||
{/* CTAs */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 lg:justify-start">
|
||||
<a
|
||||
href="/tickets"
|
||||
className="rounded-2xl bg-acid-lime px-8 py-3 text-base font-semibold text-tech-navy transition-opacity hover:opacity-90"
|
||||
>
|
||||
Jetzt Ticket sichern
|
||||
</a>
|
||||
<a
|
||||
href="/programm"
|
||||
className="rounded-2xl border border-cloud-white/25 px-8 py-3 text-base font-semibold text-cloud-white transition-colors hover:border-cloud-white/50 hover:bg-white/5"
|
||||
>
|
||||
Programm ansehen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="mt-14 flex flex-wrap items-center justify-center gap-8 lg:justify-start md:gap-12">
|
||||
{[
|
||||
{ value: "2", label: "Tage" },
|
||||
{ value: "12+", label: "Sessions" },
|
||||
{ value: "6", label: "Workshops" },
|
||||
{ value: "500+", label: "Teilnehmer" },
|
||||
].map((stat, i) => (
|
||||
<div key={i} className="text-center lg:text-left">
|
||||
<div className="text-2xl font-bold text-acid-lime md:text-3xl">{stat.value}</div>
|
||||
<div className="mt-0.5 text-[11px] uppercase tracking-widest text-cloud-white/40">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<img src={image.src} className="w-full object-cover" alt={image.alt} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Speaker companies bar */}
|
||||
<div className="relative z-10 border-t border-white/10 px-[5%] py-5">
|
||||
<div className="container">
|
||||
<div className="flex flex-wrap items-center justify-center gap-6 md:gap-10 lg:justify-start">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-[0.18em] text-cloud-white/30">
|
||||
Speaker von
|
||||
</span>
|
||||
<img src={logoAdobe} alt="Adobe" className="h-4 brightness-0 invert opacity-35" />
|
||||
<img src={logoFigma} alt="Figma" className="h-4 brightness-0 invert opacity-35" />
|
||||
<img src={logoGoogle} alt="Google" className="h-4 brightness-0 invert opacity-35" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -45,13 +116,6 @@ export const Header1 = (props: Header1Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Header1Defaults: Props = {
|
||||
heading: "UXplore Konferenz 2026",
|
||||
description:
|
||||
"25-26 Juni Zürich",
|
||||
buttons: [],
|
||||
image: {
|
||||
src: headerImage,
|
||||
alt: "UXplore Konferenz 2026",
|
||||
},
|
||||
};
|
||||
export const Header1Defaults = {};
|
||||
|
||||
export default Header1;
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
import headerBg from "../assets/program-header.png";
|
||||
import { Button } from "@relume_io/relume-ui";
|
||||
import type { ButtonProps } from "@relume_io/relume-ui";
|
||||
type LinkButton = {
|
||||
title: string;
|
||||
href?: string;
|
||||
variant?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
heading: string;
|
||||
description: string;
|
||||
buttons: ButtonProps[];
|
||||
buttons: LinkButton[];
|
||||
showTicketButton?: boolean;
|
||||
};
|
||||
|
||||
export type Header23Props = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;
|
||||
|
||||
export const Header23 = (props: Header23Props) => {
|
||||
const { heading, description, buttons, children } = {
|
||||
const { heading, description, buttons, showTicketButton = true, children } = {
|
||||
...Header23Defaults,
|
||||
...props,
|
||||
};
|
||||
@ -29,18 +34,38 @@ export const Header23 = (props: Header23Props) => {
|
||||
{/* Content */}
|
||||
<div className="container relative z-10">
|
||||
<div className="mx-auto w-full max-w-lg text-center">
|
||||
<div className="mb-5 flex items-center justify-center gap-3 md:mb-6">
|
||||
<span className="rounded-full bg-white/15 px-4 py-1.5 text-sm font-semibold text-cloud-white">
|
||||
2-Tage-Konferenz
|
||||
</span>
|
||||
<span className="rounded-full border border-acid-lime bg-acid-lime/10 px-4 py-1.5 text-sm font-semibold text-acid-lime">
|
||||
Workshops inklusive
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="mb-5 text-6xl font-bold text-cloud-white md:mb-6 md:text-9xl lg:text-10xl">
|
||||
{heading}
|
||||
</h1>
|
||||
<p className="md:text-md text-cloud-white text-[18px] leading-[1.5]">
|
||||
{description}
|
||||
</p>
|
||||
{showTicketButton && (
|
||||
<a
|
||||
href="/tickets"
|
||||
className="mt-8 inline-block rounded-2xl bg-acid-lime px-8 py-3 text-base font-normal text-tech-navy hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Jetzt Ticket sichern
|
||||
</a>
|
||||
)}
|
||||
{buttons.length > 0 && (
|
||||
<div className="mt-6 flex items-center justify-center gap-x-4 md:mt-8">
|
||||
<div className="mt-4 flex items-center justify-center gap-x-4">
|
||||
{buttons.map((button, index) => (
|
||||
<Button key={index} {...button}>
|
||||
<a
|
||||
key={index}
|
||||
href={button.href ?? "#"}
|
||||
className={`inline-flex items-center justify-center rounded-2xl border px-6 py-3 text-sm font-semibold transition-colors ${button.className ?? ""}`}
|
||||
>
|
||||
{button.title}
|
||||
</Button>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@ -53,7 +78,7 @@ export const Header23 = (props: Header23Props) => {
|
||||
|
||||
export const Header23Defaults: Props = {
|
||||
heading: "UXplore 2026 Programm",
|
||||
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.",
|
||||
description: "Erlebe zwei Tage voller Insights von den klügsten Köpfen der Branche. Jedes Ticket beinhaltet den Zugang zu allen Sessions und Workshops – nach dem Kauf kannst du dich für deine Wunsch-Workshops anmelden.",
|
||||
buttons: [],
|
||||
};
|
||||
|
||||
|
||||
@ -95,10 +95,10 @@ export const Navbar3 = (props: Navbar3Props) => {
|
||||
{buttons.map(({ url, ...button }, index) =>
|
||||
url ? (
|
||||
<Link key={index} to={url}>
|
||||
<Button {...button} className="w-full">{button.title}</Button>
|
||||
<Button {...button} className="w-full rounded-2xl bg-electric-violet border-electric-violet text-cloud-white font-barlow font-semibold">{button.title}</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<Button key={index} {...button} className="w-full">{button.title}</Button>
|
||||
<Button key={index} {...button} className="w-full rounded-2xl bg-electric-violet border-electric-violet text-cloud-white font-barlow font-semibold">{button.title}</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -2,8 +2,10 @@ import React, { useState } from "react";
|
||||
|
||||
type TicketPlan = {
|
||||
day: string;
|
||||
price: number;
|
||||
date: string;
|
||||
earlyBirdPrice: number;
|
||||
regularPrice: number;
|
||||
earlyBirdDeadline: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@ -13,15 +15,28 @@ type Props = {
|
||||
|
||||
export type Pricing39TicketProps = React.ComponentPropsWithoutRef<"section"> & Partial<Props>;
|
||||
|
||||
const TicketCardLime = ({ plan }: { plan: TicketPlan }) => {
|
||||
const TicketCard = ({ plan }: { plan: TicketPlan }) => {
|
||||
const [qty, setQty] = useState(0);
|
||||
const total = qty * plan.price;
|
||||
const total = qty * plan.earlyBirdPrice;
|
||||
|
||||
return (
|
||||
<div className="bg-acid-lime text-tech-navy p-6 md:p-8 flex flex-col gap-5 font-barlow border border-electric-violet">
|
||||
<p className="text-sm font-semibold">{plan.day}</p>
|
||||
<h3 className="text-6xl font-bold md:text-8xl">CHF {plan.price}</h3>
|
||||
<p className="text-sm">{plan.date}</p>
|
||||
<div className="bg-electric-violet/10 text-tech-navy p-6 md:p-8 flex flex-col gap-5 font-barlow border border-electric-violet/30">
|
||||
<p className="text-sm font-semibold uppercase tracking-wide">{plan.day}</p>
|
||||
|
||||
{/* Earlybird price block */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-end gap-3">
|
||||
<h3 className="text-6xl font-bold md:text-8xl leading-none">CHF {plan.earlyBirdPrice}</h3>
|
||||
<span className="mb-1 text-sm text-tech-navy/50 line-through">CHF {plan.regularPrice}</span>
|
||||
</div>
|
||||
<span className="inline-flex w-fit items-center gap-1.5 rounded-full bg-acid-lime px-3 py-1 text-xs font-semibold text-tech-navy">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-tech-navy" />
|
||||
Earlybird bis {plan.earlyBirdDeadline}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-tech-navy/50">{plan.date}</p>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
@ -39,10 +54,11 @@ const TicketCardLime = ({ plan }: { plan: TicketPlan }) => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-right text-sm font-semibold">
|
||||
<div>CHF {plan.price},-</div>
|
||||
<div>{total},-</div>
|
||||
<div className="text-tech-navy/50">CHF {plan.earlyBirdPrice},-</div>
|
||||
<div>Total CHF {total},-</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="w-full rounded-2xl bg-electric-violet text-cloud-white font-semibold py-3 text-sm">
|
||||
Ticket sichern
|
||||
</button>
|
||||
@ -69,7 +85,7 @@ export const Pricing39Ticket = (props: Pricing39TicketProps) => {
|
||||
}`}
|
||||
>
|
||||
{plans.map((plan, index) => (
|
||||
<TicketCardLime key={index} plan={plan} />
|
||||
<TicketCard key={index} plan={plan} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@ -80,8 +96,8 @@ export const Pricing39Ticket = (props: Pricing39TicketProps) => {
|
||||
export const Pricing39TicketDefaults: Props = {
|
||||
heading: "1-Tagespässe",
|
||||
plans: [
|
||||
{ day: "Donnerstag", price: 131, date: "25. Juni 2026" },
|
||||
{ day: "Freitag", price: 131, date: "26. Juni 2026" },
|
||||
{ day: "Donnerstag", date: "25. Juni 2026", earlyBirdPrice: 99, regularPrice: 131, earlyBirdDeadline: "31. Mai 2026" },
|
||||
{ day: "Freitag", date: "26. Juni 2026", earlyBirdPrice: 99, regularPrice: 131, earlyBirdDeadline: "31. Mai 2026" },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@ -81,17 +81,7 @@ const MemberCard = ({ member }: { member: TeamMember }) => (
|
||||
<p className="text-lg text-tech-navy">{member.company}</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="mb-4 text-base leading-relaxed text-tech-navy">{member.description}</p>
|
||||
<div className="mb-4 flex flex-wrap gap-2">
|
||||
{member.talks.map((talk, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`rounded-lg px-2 py-1 text-sm font-semibold whitespace-nowrap ${talkBadgeClass(talk.type)}`}
|
||||
>
|
||||
{talk.title}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-base leading-relaxed text-tech-navy">{member.description}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@ -6,45 +6,61 @@ import { Event33 } from "../components/Event33";
|
||||
import { Banner15 } from "../components/Banner15";
|
||||
import { Footer3 } from "../components/Footer";
|
||||
import { ALL_EVENTS } from "../data/events";
|
||||
import type { EventDay } from "../components/Event33";
|
||||
|
||||
const TYPE_TABS = ["Talk", "Workshop", "Interaction", "AI and Future"];
|
||||
const LEVEL_TABS: Record<string, string> = {
|
||||
"Senior": "Senior",
|
||||
"Mid-Level": "Mid-Level",
|
||||
"Junior": "Junior",
|
||||
const DAY_VALUES = ["Donnerstag", "Freitag"];
|
||||
const TYPE_VALUES = ["Talk", "Workshop", "Interaction", "AI and Future"];
|
||||
const LEVEL_VALUES = ["Senior", "Mid-Level", "Junior"];
|
||||
|
||||
const DAY_ORDER: Record<EventDay, number> = { Donnerstag: 0, Freitag: 1 };
|
||||
|
||||
const parseStartMinutes = (time: string) => {
|
||||
const match = time.match(/(\d{2}):(\d{2})/);
|
||||
return match ? parseInt(match[1]) * 60 + parseInt(match[2]) : 0;
|
||||
};
|
||||
|
||||
const Programm = () => {
|
||||
const [activeTab, setActiveTab] = useState("all");
|
||||
const [activeTabs, setActiveTabs] = useState<string[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [activeDay, setActiveDay] = useState("all");
|
||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
|
||||
|
||||
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;
|
||||
const filtered = ALL_EVENTS.filter((e) => {
|
||||
if (activeTabs.length > 0) {
|
||||
const activeDays = activeTabs.filter((t) => DAY_VALUES.includes(t));
|
||||
const activeTypes = activeTabs.filter((t) => TYPE_VALUES.includes(t));
|
||||
const activeLevels = activeTabs.filter((t) => LEVEL_VALUES.includes(t));
|
||||
if (activeDays.length > 0 && !activeDays.includes(e.day)) return false;
|
||||
if (activeTypes.length > 0 && !activeTypes.includes(e.type)) return false;
|
||||
if (activeLevels.length > 0 && !activeLevels.includes(e.level)) 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]);
|
||||
|
||||
return [...filtered].sort((a, b) => {
|
||||
const dayDiff = DAY_ORDER[a.day] - DAY_ORDER[b.day];
|
||||
if (dayDiff !== 0) return sortOrder === "asc" ? dayDiff : -dayDiff;
|
||||
const timeDiff = parseStartMinutes(a.time) - parseStartMinutes(b.time);
|
||||
return sortOrder === "asc" ? timeDiff : -timeDiff;
|
||||
});
|
||||
}, [activeTabs, searchQuery, sortOrder]);
|
||||
|
||||
const activeTags = [
|
||||
searchQuery ? { label: searchQuery, onRemove: () => setSearchQuery("") } : null,
|
||||
activeDay !== "all" ? { label: activeDay, onRemove: () => setActiveDay("all") } : null,
|
||||
activeTab !== "all" ? { label: activeTab, onRemove: () => setActiveTab("all") } : null,
|
||||
...activeTabs.map((tab) => ({
|
||||
label: tab,
|
||||
onRemove: () => setActiveTabs((prev) => prev.filter((t) => t !== tab)),
|
||||
})),
|
||||
].filter(Boolean) as { label: string; onRemove: () => void }[];
|
||||
|
||||
const handleReset = () => {
|
||||
setActiveTab("all");
|
||||
setActiveTabs([]);
|
||||
setSearchQuery("");
|
||||
setActiveDay("all");
|
||||
setSortOrder("asc");
|
||||
};
|
||||
|
||||
return (
|
||||
@ -52,14 +68,14 @@ const Programm = () => {
|
||||
<Navbar3 />
|
||||
<Header23 />
|
||||
<Filters5
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
activeTabs={activeTabs}
|
||||
onTabsChange={setActiveTabs}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
activeDay={activeDay}
|
||||
onDayChange={setActiveDay}
|
||||
activeTags={activeTags}
|
||||
onReset={handleReset}
|
||||
sortOrder={sortOrder}
|
||||
onSortChange={setSortOrder}
|
||||
/>
|
||||
<Event33 events={filteredEvents} />
|
||||
<Banner15 />
|
||||
|
||||
@ -7,7 +7,6 @@ import { Team4Defaults } from "../components/Team4";
|
||||
import { Footer3 } from "../components/Footer";
|
||||
|
||||
const SPEAKER_TABS = [
|
||||
{ value: "all", label: "Alle ansehen" },
|
||||
{ value: "Strategie", label: "Strategie" },
|
||||
{ value: "Research", label: "Research" },
|
||||
{ value: "Design / UX", label: "Design / UX" },
|
||||
@ -18,12 +17,12 @@ const SPEAKER_TABS = [
|
||||
const ALL_SPEAKERS = Team4Defaults.teamMembers;
|
||||
|
||||
const Speaker = () => {
|
||||
const [activeTab, setActiveTab] = useState("all");
|
||||
const [activeTabs, setActiveTabs] = useState<string[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const filteredSpeakers = useMemo(() => {
|
||||
return ALL_SPEAKERS.filter((s) => {
|
||||
if (activeTab !== "all" && s.category !== activeTab) return false;
|
||||
if (activeTabs.length > 0 && !activeTabs.includes(s.category)) return false;
|
||||
if (searchQuery) {
|
||||
const q = searchQuery.toLowerCase();
|
||||
if (
|
||||
@ -35,11 +34,14 @@ const Speaker = () => {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [activeTab, searchQuery]);
|
||||
}, [activeTabs, searchQuery]);
|
||||
|
||||
const activeTags = [
|
||||
searchQuery ? { label: searchQuery, onRemove: () => setSearchQuery("") } : null,
|
||||
activeTab !== "all" ? { label: activeTab, onRemove: () => setActiveTab("all") } : null,
|
||||
...activeTabs.map((tab) => ({
|
||||
label: tab,
|
||||
onRemove: () => setActiveTabs((prev) => prev.filter((t) => t !== tab)),
|
||||
})),
|
||||
].filter(Boolean) as { label: string; onRemove: () => void }[];
|
||||
|
||||
return (
|
||||
@ -49,20 +51,19 @@ const Speaker = () => {
|
||||
heading="Unsere Speaker"
|
||||
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={[
|
||||
{ title: "Zu den Speakern", variant: "primary", className: "bg-acid-lime text-neutral-dark border-acid-lime hover:bg-acid-lime/90 hover:border-acid-lime/90 rounded-2xl" },
|
||||
{ title: "Zum Programm", variant: "secondary", className: "bg-transparent border-acid-lime text-acid-lime hover:bg-acid-lime/10 hover:border-acid-lime hover:text-acid-lime rounded-2xl" },
|
||||
{ title: "Zum Programm", href: "/programm", className: "bg-transparent border-acid-lime text-acid-lime hover:bg-acid-lime/10" },
|
||||
]}
|
||||
/>
|
||||
<Filters5
|
||||
tabs={SPEAKER_TABS}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
activeTabs={activeTabs}
|
||||
onTabsChange={setActiveTabs}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
searchLabel="Stichwortsuche"
|
||||
resetLabel="Löschen"
|
||||
activeTags={activeTags}
|
||||
onReset={() => { setSearchQuery(""); setActiveTab("all"); }}
|
||||
onReset={() => { setSearchQuery(""); setActiveTabs([]); }}
|
||||
useFilterToggle={false}
|
||||
showDayFilter={false}
|
||||
showSortButton={false}
|
||||
|
||||
@ -2,6 +2,7 @@ import { useParams, Navigate } from "react-router-dom";
|
||||
import { Navbar3 } from "../components/Navbar";
|
||||
import { Team4, Team4Defaults } from "../components/Team4";
|
||||
import { Footer3 } from "../components/Footer";
|
||||
import { ALL_EVENTS } from "../data/events";
|
||||
import webflowLogo from "../assets/webflow.png";
|
||||
import relumeLogo from "../assets/relume.png";
|
||||
|
||||
@ -38,6 +39,7 @@ const SpeakerDetail = () => {
|
||||
|
||||
if (!speaker) return <Navigate to="/speaker" replace />;
|
||||
|
||||
const speakerEvents = ALL_EVENTS.filter((e) => e.speaker === speaker.name);
|
||||
const otherSpeakers = ALL_SPEAKERS.filter((s) => s.slug !== slug);
|
||||
|
||||
return (
|
||||
@ -95,6 +97,41 @@ const SpeakerDetail = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sessions */}
|
||||
{speakerEvents.length > 0 && (
|
||||
<section className="bg-cloud-white px-[5%] py-16 font-barlow">
|
||||
<div className="container">
|
||||
<h2 className="mb-8 text-3xl font-bold text-tech-navy md:text-4xl">Sessions</h2>
|
||||
<div className="flex flex-col gap-4">
|
||||
{speakerEvents.map((event) => (
|
||||
<a
|
||||
key={event.id}
|
||||
href={`/programm/${event.id}`}
|
||||
className="group flex flex-col gap-3 rounded-2xl border border-gray-200 p-6 transition-colors hover:border-electric-violet sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="rounded-md bg-electric-violet/10 px-3 py-1 text-xs font-semibold text-electric-violet">
|
||||
{event.type}
|
||||
</span>
|
||||
<span className="text-sm text-gray-400">
|
||||
{event.day} · {event.time}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-base font-semibold text-tech-navy group-hover:text-electric-violet transition-colors">
|
||||
{event.title}
|
||||
</p>
|
||||
</div>
|
||||
<span className="shrink-0 text-sm font-semibold text-electric-violet">
|
||||
Zur Session →
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Other speakers */}
|
||||
<div className="bg-cloud-white">
|
||||
<div className="px-[5%] pt-16 font-barlow">
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Navbar3 } from "../components/Navbar";
|
||||
import { Header23 } from "../components/Header23";
|
||||
import { Pricing1Ticket } from "../components/Pricing1Ticket";
|
||||
import { Pricing39Ticket } from "../components/Pricing39Ticket";
|
||||
import { Footer3 } from "../components/Footer";
|
||||
import { Input } from "@relume_io/relume-ui";
|
||||
@ -82,6 +81,7 @@ const Tickets = () => {
|
||||
heading="Ticketshop"
|
||||
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={[]}
|
||||
showTicketButton={false}
|
||||
>
|
||||
<div className="mt-5 font-semibold text-cloud-white md:mt-6 md:text-md">
|
||||
DO – FR, 25. – 26. Juni 2026
|
||||
@ -120,17 +120,16 @@ const Tickets = () => {
|
||||
</p>
|
||||
</div>
|
||||
</Header23>
|
||||
<Pricing1Ticket />
|
||||
<Pricing39Ticket
|
||||
<Pricing39Ticket
|
||||
heading="1-Tagespässe"
|
||||
plans={[
|
||||
{ day: "Donnerstag", price: 131, date: "25. Juni 2026" },
|
||||
{ day: "Freitag", price: 131, date: "26. Juni 2026" },
|
||||
{ day: "Donnerstag", date: "25. Juni 2026", earlyBirdPrice: 99, regularPrice: 131, earlyBirdDeadline: "31. Mai 2026" },
|
||||
{ day: "Freitag", date: "26. Juni 2026", earlyBirdPrice: 99, regularPrice: 131, earlyBirdDeadline: "31. Mai 2026" },
|
||||
]}
|
||||
/>
|
||||
<Pricing39Ticket
|
||||
heading="2-Tagespässe"
|
||||
plans={[{ day: "Donnerstag – Freitag", price: 236, date: "25. – 26. Juni 2026" }]}
|
||||
heading="2-Tagespass"
|
||||
plans={[{ day: "Donnerstag – Freitag", date: "25. – 26. Juni 2026", earlyBirdPrice: 179, regularPrice: 236, earlyBirdDeadline: "31. Mai 2026" }]}
|
||||
/>
|
||||
<Footer3 />
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user