Use four-step carousel per program
This commit is contained in:
111
src/App.jsx
111
src/App.jsx
@@ -523,17 +523,23 @@ function FlowRow({
|
||||
return 'disabled';
|
||||
};
|
||||
const isRowClickable = Boolean(onLabelClick) && !isEditing;
|
||||
const isWrappedStepLayout = steps.length > 5;
|
||||
const isSingleStepLayout = steps.length === 1;
|
||||
const stepRows = isWrappedStepLayout
|
||||
? steps.reduce((rows, step, index) => {
|
||||
const rowIndex = Math.floor(index / 4);
|
||||
rows[rowIndex] = [...(rows[rowIndex] ?? []), { step, index }];
|
||||
return rows;
|
||||
}, [])
|
||||
: [];
|
||||
const wrappedCardColumns = ['xl:col-start-1', 'xl:col-start-3', 'xl:col-start-5', 'xl:col-start-7'];
|
||||
const wrappedArrowColumns = ['xl:col-start-2', 'xl:col-start-4', 'xl:col-start-6'];
|
||||
const maxVisibleSteps = 4;
|
||||
const [stepWindowStart, setStepWindowStart] = useState(0);
|
||||
const maxWindowStart = Math.max(0, steps.length - maxVisibleSteps);
|
||||
const visibleStepItems = steps
|
||||
.map((step, index) => ({ step, index }))
|
||||
.slice(stepWindowStart, stepWindowStart + maxVisibleSteps);
|
||||
const placeholderCount = Math.max(0, maxVisibleSteps - visibleStepItems.length);
|
||||
const canMovePrev = stepWindowStart > 0;
|
||||
const canMoveNext = stepWindowStart < maxWindowStart;
|
||||
|
||||
useEffect(() => {
|
||||
setStepWindowStart((current) => Math.min(current, maxWindowStart));
|
||||
}, [maxWindowStart]);
|
||||
|
||||
const moveStepWindow = (direction) => {
|
||||
setStepWindowStart((current) => Math.min(maxWindowStart, Math.max(0, current + direction)));
|
||||
};
|
||||
|
||||
const renderStepCard = (step, index, className) => (
|
||||
<div className={`relative z-10 flex flex-col gap-2 ${className}`}>
|
||||
@@ -687,46 +693,59 @@ function FlowRow({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isWrappedStepLayout ? (
|
||||
<div className="space-y-5">
|
||||
{stepRows.map((row, rowIndex) => (
|
||||
<div
|
||||
key={rowIndex}
|
||||
className="relative flex flex-col gap-3 xl:grid xl:grid-cols-[minmax(0,1fr)_28px_minmax(0,1fr)_28px_minmax(0,1fr)_28px_minmax(0,1fr)] xl:gap-0 xl:items-stretch"
|
||||
<div className="relative overflow-hidden rounded-[24px] bg-white/25 px-9 py-1 ring-1 ring-white/70">
|
||||
{steps.length > maxVisibleSteps && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canMovePrev}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
moveStepWindow(-1);
|
||||
}}
|
||||
className="absolute left-2 top-1/2 z-20 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-white/95 text-slate-500 shadow-sm ring-1 ring-slate-200 hover:bg-slate-50 disabled:opacity-25"
|
||||
aria-label="이전 스텝 보기"
|
||||
>
|
||||
{row.map(({ step, index }, itemIndex) => (
|
||||
<React.Fragment key={step.id}>
|
||||
{renderStepCard(
|
||||
step,
|
||||
index,
|
||||
`min-w-0 ${wrappedCardColumns[itemIndex]}`
|
||||
)}
|
||||
{itemIndex < row.length - 1 && (
|
||||
<div className={`flex items-center justify-center ${wrappedArrowColumns[itemIndex]}`}>
|
||||
<ArrowRight className={`hidden h-5 w-5 xl:block ${accent.arrowText}`} />
|
||||
<ArrowDown className={`h-5 w-5 xl:hidden ${accent.arrowText}`} />
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canMoveNext}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
moveStepWindow(1);
|
||||
}}
|
||||
className="absolute right-2 top-1/2 z-20 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full bg-white/95 text-slate-500 shadow-sm ring-1 ring-slate-200 hover:bg-slate-50 disabled:opacity-25"
|
||||
aria-label="다음 스텝 보기"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<div className="relative grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4 xl:gap-5">
|
||||
{visibleStepItems.map(({ step, index }, itemIndex) => (
|
||||
<div key={step.id} className="relative">
|
||||
{renderStepCard(step, index, 'min-w-0')}
|
||||
{itemIndex < visibleStepItems.length - 1 && (
|
||||
<ArrowDown className={`mx-auto mt-2 h-5 w-5 xl:hidden ${accent.arrowText}`} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative flex flex-col gap-3 xl:flex-row xl:items-stretch">
|
||||
{steps.map((step, index) => (
|
||||
<React.Fragment key={step.id}>
|
||||
{renderStepCard(step, index, isSingleStepLayout ? 'w-full max-w-[320px]' : 'min-w-0 flex-1')}
|
||||
{index < steps.length - 1 && (
|
||||
<div className="flex items-center justify-center xl:w-7">
|
||||
<ArrowRight className={`hidden h-5 w-5 xl:block ${accent.arrowText}`} />
|
||||
<ArrowDown className={`h-5 w-5 xl:hidden ${accent.arrowText}`} />
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
{Array.from({ length: placeholderCount }).map((_, index) => (
|
||||
<div
|
||||
key={`placeholder-${index}`}
|
||||
className="hidden min-h-[220px] rounded-2xl border border-dashed border-transparent xl:block"
|
||||
/>
|
||||
))}
|
||||
{visibleStepItems.slice(0, -1).map((item, index) => (
|
||||
<ArrowRight
|
||||
key={`arrow-${item.step.id}`}
|
||||
className={`pointer-events-none absolute top-1/2 z-20 hidden h-5 w-5 -translate-x-1/2 -translate-y-1/2 xl:block ${accent.arrowText}`}
|
||||
style={{ left: `${25 * (index + 1)}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user