feat: FAQ two-column layout, team swipe & counter, showcase improvements
- FAQ: split into 2 columns on desktop to reduce section height - Team: remove description line-clamp, show full bio - ShowcaseLayout: add swipe navigation on mobile, optional counter - Counter shows current/total with gold accent styling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { useRef, useEffect, useState, useCallback } from "react";
|
||||
|
||||
interface ShowcaseLayoutProps<T> {
|
||||
items: T[];
|
||||
@@ -9,6 +9,7 @@ interface ShowcaseLayoutProps<T> {
|
||||
onHoverChange?: (hovering: boolean) => void;
|
||||
renderDetail: (item: T, index: number) => React.ReactNode;
|
||||
renderSelectorItem: (item: T, index: number, isActive: boolean) => React.ReactNode;
|
||||
counter?: boolean;
|
||||
}
|
||||
|
||||
export function ShowcaseLayout<T>({
|
||||
@@ -18,9 +19,11 @@ export function ShowcaseLayout<T>({
|
||||
onHoverChange,
|
||||
renderDetail,
|
||||
renderSelectorItem,
|
||||
counter = false,
|
||||
}: ShowcaseLayoutProps<T>) {
|
||||
const selectorRef = useRef<HTMLDivElement>(null);
|
||||
const activeItemRef = useRef<HTMLButtonElement>(null);
|
||||
const detailRef = useRef<HTMLDivElement>(null);
|
||||
const [isUserInteracting, setIsUserInteracting] = useState(false);
|
||||
|
||||
// Auto-scroll selector only when item is out of view
|
||||
@@ -56,6 +59,32 @@ export function ShowcaseLayout<T>({
|
||||
}
|
||||
}, [activeIndex, isUserInteracting]);
|
||||
|
||||
// Swipe support on detail area
|
||||
const touchStart = useRef<{ x: number; y: number } | null>(null);
|
||||
|
||||
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
||||
touchStart.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
||||
}, []);
|
||||
|
||||
const handleTouchEnd = useCallback(
|
||||
(e: React.TouchEvent) => {
|
||||
if (!touchStart.current) return;
|
||||
const dx = e.changedTouches[0].clientX - touchStart.current.x;
|
||||
const dy = e.changedTouches[0].clientY - touchStart.current.y;
|
||||
touchStart.current = null;
|
||||
|
||||
// Only trigger if horizontal swipe is dominant and > 50px
|
||||
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy) * 1.5) {
|
||||
if (dx < 0 && activeIndex < items.length - 1) {
|
||||
onSelect(activeIndex + 1);
|
||||
} else if (dx > 0 && activeIndex > 0) {
|
||||
onSelect(activeIndex - 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
[activeIndex, items.length, onSelect],
|
||||
);
|
||||
|
||||
function handleMouseEnter() {
|
||||
setIsUserInteracting(true);
|
||||
onHoverChange?.(true);
|
||||
@@ -74,9 +103,28 @@ export function ShowcaseLayout<T>({
|
||||
>
|
||||
{/* Detail area */}
|
||||
<div className="lg:w-[60%]">
|
||||
<div key={activeIndex} className="showcase-detail-enter">
|
||||
<div
|
||||
ref={detailRef}
|
||||
key={activeIndex}
|
||||
className="showcase-detail-enter"
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
>
|
||||
{renderDetail(items[activeIndex], activeIndex)}
|
||||
</div>
|
||||
|
||||
{/* Counter */}
|
||||
{counter && (
|
||||
<div className="mt-3 flex items-center justify-center gap-2 lg:justify-start">
|
||||
<span className="text-xs font-medium tabular-nums text-[#c9a96e]">
|
||||
{String(activeIndex + 1).padStart(2, "0")}
|
||||
</span>
|
||||
<span className="h-[1px] w-8 bg-white/10" />
|
||||
<span className="text-xs tabular-nums text-neutral-500">
|
||||
{String(items.length).padStart(2, "0")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Selector */}
|
||||
|
||||
Reference in New Issue
Block a user