Use four-step carousel per program
This commit is contained in:
107
src/App.jsx
107
src/App.jsx
@@ -523,17 +523,23 @@ function FlowRow({
|
|||||||
return 'disabled';
|
return 'disabled';
|
||||||
};
|
};
|
||||||
const isRowClickable = Boolean(onLabelClick) && !isEditing;
|
const isRowClickable = Boolean(onLabelClick) && !isEditing;
|
||||||
const isWrappedStepLayout = steps.length > 5;
|
const maxVisibleSteps = 4;
|
||||||
const isSingleStepLayout = steps.length === 1;
|
const [stepWindowStart, setStepWindowStart] = useState(0);
|
||||||
const stepRows = isWrappedStepLayout
|
const maxWindowStart = Math.max(0, steps.length - maxVisibleSteps);
|
||||||
? steps.reduce((rows, step, index) => {
|
const visibleStepItems = steps
|
||||||
const rowIndex = Math.floor(index / 4);
|
.map((step, index) => ({ step, index }))
|
||||||
rows[rowIndex] = [...(rows[rowIndex] ?? []), { step, index }];
|
.slice(stepWindowStart, stepWindowStart + maxVisibleSteps);
|
||||||
return rows;
|
const placeholderCount = Math.max(0, maxVisibleSteps - visibleStepItems.length);
|
||||||
}, [])
|
const canMovePrev = stepWindowStart > 0;
|
||||||
: [];
|
const canMoveNext = stepWindowStart < maxWindowStart;
|
||||||
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'];
|
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) => (
|
const renderStepCard = (step, index, className) => (
|
||||||
<div className={`relative z-10 flex flex-col gap-2 ${className}`}>
|
<div className={`relative z-10 flex flex-col gap-2 ${className}`}>
|
||||||
@@ -687,46 +693,59 @@ function FlowRow({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isWrappedStepLayout ? (
|
<div className="relative overflow-hidden rounded-[24px] bg-white/25 px-9 py-1 ring-1 ring-white/70">
|
||||||
<div className="space-y-5">
|
{steps.length > maxVisibleSteps && (
|
||||||
{stepRows.map((row, rowIndex) => (
|
<>
|
||||||
<div
|
<button
|
||||||
key={rowIndex}
|
type="button"
|
||||||
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"
|
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}>
|
</button>
|
||||||
{renderStepCard(
|
<button
|
||||||
step,
|
type="button"
|
||||||
index,
|
disabled={!canMoveNext}
|
||||||
`min-w-0 ${wrappedCardColumns[itemIndex]}`
|
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>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{itemIndex < row.length - 1 && (
|
<div className="relative grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4 xl:gap-5">
|
||||||
<div className={`flex items-center justify-center ${wrappedArrowColumns[itemIndex]}`}>
|
{visibleStepItems.map(({ step, index }, itemIndex) => (
|
||||||
<ArrowRight className={`hidden h-5 w-5 xl:block ${accent.arrowText}`} />
|
<div key={step.id} className="relative">
|
||||||
<ArrowDown className={`h-5 w-5 xl:hidden ${accent.arrowText}`} />
|
{renderStepCard(step, index, 'min-w-0')}
|
||||||
</div>
|
{itemIndex < visibleStepItems.length - 1 && (
|
||||||
|
<ArrowDown className={`mx-auto mt-2 h-5 w-5 xl:hidden ${accent.arrowText}`} />
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
{Array.from({ length: placeholderCount }).map((_, index) => (
|
||||||
) : (
|
<div
|
||||||
<div className="relative flex flex-col gap-3 xl:flex-row xl:items-stretch">
|
key={`placeholder-${index}`}
|
||||||
{steps.map((step, index) => (
|
className="hidden min-h-[220px] rounded-2xl border border-dashed border-transparent xl:block"
|
||||||
<React.Fragment key={step.id}>
|
/>
|
||||||
{renderStepCard(step, index, isSingleStepLayout ? 'w-full max-w-[320px]' : 'min-w-0 flex-1')}
|
))}
|
||||||
{index < steps.length - 1 && (
|
{visibleStepItems.slice(0, -1).map((item, index) => (
|
||||||
<div className="flex items-center justify-center xl:w-7">
|
<ArrowRight
|
||||||
<ArrowRight className={`hidden h-5 w-5 xl:block ${accent.arrowText}`} />
|
key={`arrow-${item.step.id}`}
|
||||||
<ArrowDown className={`h-5 w-5 xl:hidden ${accent.arrowText}`} />
|
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}`}
|
||||||
</div>
|
style={{ left: `${25 * (index + 1)}%` }}
|
||||||
)}
|
/>
|
||||||
</React.Fragment>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user