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 { useGuestIdentity } from '../context/GuestIdentityContext';
|
||||||
import { useEventData } from '../hooks/useEventData';
|
import { useEventData } from '../hooks/useEventData';
|
||||||
import { useGuestTaskProgress } from '../hooks/useGuestTaskProgress';
|
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 { useTranslation, type TranslateFn } from '../i18n/useTranslation';
|
||||||
import { useEventBranding } from '../context/EventBrandingContext';
|
import { useEventBranding } from '../context/EventBrandingContext';
|
||||||
import type { EventBranding } from '../types/event-branding';
|
import type { EventBranding } from '../types/event-branding';
|
||||||
@@ -482,6 +482,49 @@ function MissionActionCard({
|
|||||||
const shellRadius = `${radius + 10}px`;
|
const shellRadius = `${radius + 10}px`;
|
||||||
const normalizeText = (value: string | undefined | null) =>
|
const normalizeText = (value: string | undefined | null) =>
|
||||||
(value ?? '').trim().toLowerCase().replace(/\s+/g, ' ');
|
(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 renderCardContent = (card: MissionPreview | null) => {
|
||||||
const theme = getEmotionTheme(card?.emotion ?? null);
|
const theme = getEmotionTheme(card?.emotion ?? null);
|
||||||
@@ -489,6 +532,15 @@ function MissionActionCard({
|
|||||||
const durationMinutes = card?.duration ?? 3;
|
const durationMinutes = card?.duration ?? 3;
|
||||||
const titleFont = headingFont ? { fontFamily: headingFont } : undefined;
|
const titleFont = headingFont ? { fontFamily: headingFont } : undefined;
|
||||||
const gradientBackground = card ? theme.gradientBackground : `linear-gradient(135deg, ${primary}, ${secondary})`;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -542,12 +594,48 @@ function MissionActionCard({
|
|||||||
|
|
||||||
<div className="mt-4 text-center">
|
<div className="mt-4 text-center">
|
||||||
{card ? (
|
{card ? (
|
||||||
|
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
|
<p
|
||||||
className="text-xl font-semibold leading-snug text-slate-900 sm:text-2xl break-words py-1"
|
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)' }}
|
style={{ ...titleFont, textShadow: '0 6px 18px rgba(15,23,42,0.28)' }}
|
||||||
>
|
>
|
||||||
{card.title}
|
{card.title}
|
||||||
</p>
|
</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 ? (
|
) : loading ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Skeleton className="mx-auto h-6 w-3/4" />
|
<Skeleton className="mx-auto h-6 w-3/4" />
|
||||||
@@ -622,8 +710,8 @@ function MissionActionCard({
|
|||||||
effect="cards"
|
effect="cards"
|
||||||
modules={[EffectCards]}
|
modules={[EffectCards]}
|
||||||
cardsEffect={{
|
cardsEffect={{
|
||||||
perSlideRotate: 2,
|
perSlideRotate: 1,
|
||||||
perSlideOffset: 4,
|
perSlideOffset: 1,
|
||||||
slideShadows: false,
|
slideShadows: false,
|
||||||
}}
|
}}
|
||||||
slidesPerView={1}
|
slidesPerView={1}
|
||||||
@@ -639,6 +727,10 @@ function MissionActionCard({
|
|||||||
}}
|
}}
|
||||||
onSlideChange={(instance) => {
|
onSlideChange={(instance) => {
|
||||||
const realIndex = typeof instance.realIndex === 'number' ? instance.realIndex : instance.activeIndex ?? 0;
|
const realIndex = typeof instance.realIndex === 'number' ? instance.realIndex : instance.activeIndex ?? 0;
|
||||||
|
if (realIndex !== lastSlideIndexRef.current) {
|
||||||
|
setExpandedTaskId(null);
|
||||||
|
}
|
||||||
|
lastSlideIndexRef.current = realIndex;
|
||||||
onIndexChange(realIndex, slides.length);
|
onIndexChange(realIndex, slides.length);
|
||||||
}}
|
}}
|
||||||
className="!pb-2"
|
className="!pb-2"
|
||||||
|
|||||||
Reference in New Issue
Block a user