card slider now looks good, not messy, card height is unified and long text can be tapped to be fully visible.

This commit is contained in:
Codex Agent
2025-12-18 14:09:18 +01:00
parent 2196346db7
commit 403ca71710

View File

@@ -12,7 +12,7 @@ import GalleryPreview from '../components/GalleryPreview';
import { useGuestIdentity } from '../context/GuestIdentityContext';
import { useEventData } from '../hooks/useEventData';
import { useGuestTaskProgress } from '../hooks/useGuestTaskProgress';
import { Sparkles, UploadCloud, X, RefreshCw, Timer } from 'lucide-react';
import { ChevronDown, Sparkles, UploadCloud, X, RefreshCw, Timer } from 'lucide-react';
import { useTranslation, type TranslateFn } from '../i18n/useTranslation';
import { useEventBranding } from '../context/EventBrandingContext';
import type { EventBranding } from '../types/event-branding';
@@ -482,6 +482,49 @@ function MissionActionCard({
const shellRadius = `${radius + 10}px`;
const normalizeText = (value: string | undefined | null) =>
(value ?? '').trim().toLowerCase().replace(/\s+/g, ' ');
const [expandedTaskId, setExpandedTaskId] = React.useState<number | null>(null);
const lastSlideIndexRef = React.useRef<number>(initialIndex);
const titleRefs = React.useRef(new Map<number, HTMLParagraphElement | null>());
const [expandableTitles, setExpandableTitles] = React.useState<Record<number, boolean>>({});
const measureTitleOverflow = React.useCallback(() => {
setExpandableTitles((prev) => {
let hasChange = false;
const next = { ...prev };
titleRefs.current.forEach((element, id) => {
if (!element || element.dataset.collapsed !== 'true') {
return;
}
const isOverflowing = element.scrollHeight > element.clientHeight + 1;
if (next[id] !== isOverflowing) {
next[id] = isOverflowing;
hasChange = true;
}
});
return hasChange ? next : prev;
});
}, []);
React.useLayoutEffect(() => {
if (typeof window === 'undefined') {
return;
}
const raf = window.requestAnimationFrame(measureTitleOverflow);
return () => window.cancelAnimationFrame(raf);
}, [measureTitleOverflow, cards, headingFont]);
React.useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const handleResize = () => {
window.requestAnimationFrame(measureTitleOverflow);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [measureTitleOverflow]);
const renderCardContent = (card: MissionPreview | null) => {
const theme = getEmotionTheme(card?.emotion ?? null);
@@ -489,6 +532,15 @@ function MissionActionCard({
const durationMinutes = card?.duration ?? 3;
const titleFont = headingFont ? { fontFamily: headingFont } : undefined;
const gradientBackground = card ? theme.gradientBackground : `linear-gradient(135deg, ${primary}, ${secondary})`;
const isExpanded = card ? expandedTaskId === card.id : false;
const isExpandable = Boolean(card && expandableTitles[card.id]);
const titleClamp = isExpanded ? '' : 'line-clamp-2 sm:line-clamp-3';
const titleClasses = `text-xl font-semibold leading-snug text-slate-900 sm:text-2xl break-words py-1 min-h-[3.75rem] sm:min-h-[4.5rem] ${titleClamp}`;
const titleId = card ? `task-title-${card.id}` : undefined;
const toggleExpanded = () => {
if (!card) return;
setExpandedTaskId((prev) => (prev === card.id ? null : card.id));
};
return (
<div
@@ -542,12 +594,48 @@ function MissionActionCard({
<div className="mt-4 text-center">
{card ? (
<p
className="text-xl font-semibold leading-snug text-slate-900 sm:text-2xl break-words py-1"
style={{ ...titleFont, textShadow: '0 6px 18px rgba(15,23,42,0.28)' }}
>
{card.title}
</p>
isExpandable ? (
<button
type="button"
onClick={toggleExpanded}
className="mx-auto flex w-full flex-col items-center gap-1 text-center"
aria-expanded={isExpanded}
aria-controls={titleId}
>
<p
id={titleId}
ref={(node) => {
if (card) {
titleRefs.current.set(card.id, node);
}
}}
data-collapsed={!isExpanded}
className={titleClasses}
style={{ ...titleFont, textShadow: '0 6px 18px rgba(15,23,42,0.28)' }}
>
{card.title}
</p>
<ChevronDown
className={`h-4 w-4 text-slate-600 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
aria-hidden
/>
<span className="sr-only">Titel ein- oder ausklappen</span>
</button>
) : (
<p
id={titleId}
ref={(node) => {
if (card) {
titleRefs.current.set(card.id, node);
}
}}
data-collapsed={!isExpanded}
className={titleClasses}
style={{ ...titleFont, textShadow: '0 6px 18px rgba(15,23,42,0.28)' }}
>
{card.title}
</p>
)
) : loading ? (
<div className="space-y-2">
<Skeleton className="mx-auto h-6 w-3/4" />
@@ -622,8 +710,8 @@ function MissionActionCard({
effect="cards"
modules={[EffectCards]}
cardsEffect={{
perSlideRotate: 2,
perSlideOffset: 4,
perSlideRotate: 1,
perSlideOffset: 1,
slideShadows: false,
}}
slidesPerView={1}
@@ -639,6 +727,10 @@ function MissionActionCard({
}}
onSlideChange={(instance) => {
const realIndex = typeof instance.realIndex === 'number' ? instance.realIndex : instance.activeIndex ?? 0;
if (realIndex !== lastSlideIndexRef.current) {
setExpandedTaskId(null);
}
lastSlideIndexRef.current = realIndex;
onIndexChange(realIndex, slides.length);
}}
className="!pb-2"