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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user