diff --git a/src/pages/transfer/actions/TransferPods.tsx b/src/pages/transfer/actions/TransferPods.tsx index 541985559..3537ccd73 100644 --- a/src/pages/transfer/actions/TransferPods.tsx +++ b/src/pages/transfer/actions/TransferPods.tsx @@ -39,7 +39,7 @@ export default function TransferPods() { case 1: return "Select Plots"; case 2: - return "Specify amount and address"; + return "Enter address"; default: return "Confirm send"; } @@ -50,13 +50,7 @@ export default function TransferPods() { case 1: return transferData.length > 0; case 2: - if (!!destination && transferNotice) { - if (transferData.length === 1) { - return transferData[0].end.gt(transferData[0].start); - } - return true; - } - return false; + return !!destination && transferNotice; default: return true; } @@ -131,8 +125,6 @@ export default function TransferPods() { ) : step === 2 ? ( { + if (transferData.length === 0) return null; + return computeSummaryRange(transferData, harvestableIndex); + }, [transferData, harvestableIndex]); + + if (!destination || !summary) { return null; } + const { totalPods, placeInLineStart, placeInLineEnd } = summary; + const isSinglePlot = transferData.length === 1; + return (
- {transferData.map((transfer) => { - const placeInLine = transfer.id.sub(harvestableIndex); - const podAmount = transfer.end.sub(transfer.start); - - return ( -
-
- {formatter.number(podAmount)} - Plot - Pods -
-
- @ - {formatter.number(placeInLine.add(transfer.start))} in Line -
-
- ); - })} +
+
+ {formatter.number(totalPods)} + Plot + Pods +
+
+ {isSinglePlot ? ( + <> + @ + {formatter.number(placeInLineStart)} in Line + + ) : ( + + between {formatter.number(placeInLineStart)} - {formatter.number(placeInLineEnd)} in Line + + )} +
+
diff --git a/src/pages/transfer/actions/pods/StepOne.tsx b/src/pages/transfer/actions/pods/StepOne.tsx index 5e08ea3c0..9568a1ceb 100644 --- a/src/pages/transfer/actions/pods/StepOne.tsx +++ b/src/pages/transfer/actions/pods/StepOne.tsx @@ -1,11 +1,11 @@ -import { TokenValue } from "@/classes/TokenValue"; -import PlotsTable from "@/components/PlotsTable"; -import { Button } from "@/components/ui/Button"; -import { Label } from "@/components/ui/Label"; -import { ToggleGroup } from "@/components/ui/ToggleGroup"; +import PodLineGraph from "@/components/PodLineGraph"; +import { MultiSlider } from "@/components/ui/Slider"; import { useFarmerField } from "@/state/useFarmerField"; +import { useHarvestableIndex } from "@/state/useFieldData"; +import { formatter } from "@/utils/format"; +import { computeTransferData, offsetToAbsoluteIndex } from "@/utils/podTransferUtils"; import { Plot } from "@/utils/types"; -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { PodTransferData } from "../TransferPods"; interface StepOneProps { @@ -13,81 +13,176 @@ interface StepOneProps { setTransferData: Dispatch>; } +function sortPlotsByIndex(plots: Plot[]): Plot[] { + return [...plots].sort((a, b) => a.index.sub(b.index).toNumber()); +} + export default function StepOne({ transferData, setTransferData }: StepOneProps) { - const [selected, setSelected] = useState(); const { plots } = useFarmerField(); + const harvestableIndex = useHarvestableIndex(); + + const [selectedPlots, setSelectedPlots] = useState([]); + const [podRange, setPodRange] = useState<[number, number]>([0, 0]); + const mountedRef = useRef(false); + + // Restore selection from existing transferData on mount useEffect(() => { - const _newPlots: string[] = []; - for (const data of transferData) { - const _plot = plots.find((plot) => plot.index.eq(data.id)); - if (_plot) { - _newPlots.push(_plot.index.toHuman()); - } + if (mountedRef.current) return; + mountedRef.current = true; + if (transferData.length === 0) return; + const restoredPlots = transferData + .map((data) => plots.find((p) => p.index.eq(data.id))) + .filter((p): p is Plot => p !== undefined); + if (restoredPlots.length > 0) { + const sorted = sortPlotsByIndex(restoredPlots); + setSelectedPlots(sorted); + const total = sorted.reduce((sum, p) => sum + p.pods.toNumber(), 0); + setPodRange([0, total]); } - setSelected(_newPlots); + // Only run on mount + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Total pods across selected plots + const totalPods = useMemo(() => { + return selectedPlots.reduce((sum, p) => sum + p.pods.toNumber(), 0); + }, [selectedPlots]); + + // Derived amount from slider range — no separate state needed + const amount = podRange[1] - podRange[0]; + + // Memoize selectedPlotIndices to avoid new array ref each render + const selectedPlotIndices = useMemo(() => selectedPlots.map((p) => p.index.toHuman()), [selectedPlots]); + + // Position info — plots are already sorted, use first/last directly + const positionInfo = useMemo(() => { + if (selectedPlots.length === 0) return null; + const first = selectedPlots[0]; + const last = selectedPlots[selectedPlots.length - 1]; + return { + start: first.index.sub(harvestableIndex), + end: last.index.add(last.pods).sub(harvestableIndex), + }; + }, [selectedPlots, harvestableIndex]); + + // Compute selectedPodRange for PodLineGraph (absolute indices) + const selectedPodRange = useMemo(() => { + if (selectedPlots.length === 0) return undefined; + return { + start: offsetToAbsoluteIndex(podRange[0], selectedPlots), + end: offsetToAbsoluteIndex(podRange[1], selectedPlots), + }; + }, [selectedPlots, podRange]); + + // Handle plot selection changes: sort, reset slider, update transferData const handlePlotSelection = useCallback( - (value: string[]) => { - // Update selected plots - setSelected(value); - - // Get selected plots data - const selectedPlots = value - .map((plotIndex) => { - const plot = plots.find((p) => p.index.toHuman() === plotIndex); - return plot; - }) - .filter((plot): plot is Plot => plot !== undefined && !plot.fullyHarvested); - - // If no valid plots selected, clear transfer data - if (selectedPlots.length === 0) { + (newPlots: Plot[]) => { + const sorted = sortPlotsByIndex(newPlots); + setSelectedPlots(sorted); + + if (sorted.length > 0) { + const newTotal = sorted.reduce((sum, p) => sum + p.pods.toNumber(), 0); + setPodRange([0, newTotal]); + setTransferData(computeTransferData(sorted, [0, newTotal])); + } else { + setPodRange([0, 0]); setTransferData([]); + } + }, + [setTransferData], + ); + + // Toggle logic: if all in group selected → deselect, else add + const handlePlotGroupSelect = useCallback( + (plotIndices: string[]) => { + const groupSet = new Set(plotIndices); + const plotsInGroup = plots.filter((p) => groupSet.has(p.index.toHuman())); + if (plotsInGroup.length === 0) return; + + const selectedSet = new Set(selectedPlots.map((p) => p.index.toHuman())); + const allSelected = plotIndices.every((idx) => selectedSet.has(idx)); + + if (allSelected) { + handlePlotSelection(selectedPlots.filter((p) => !groupSet.has(p.index.toHuman()))); return; } - // Create plot transfer data - const transferData = selectedPlots.map((plot) => { - return { - id: plot.index, - start: TokenValue.ZERO, - end: plot.pods, - }; - }); - - // Update transfer data - setTransferData(transferData); + const newPlots = [...selectedPlots]; + for (const plotToAdd of plotsInGroup) { + if (!selectedSet.has(plotToAdd.index.toHuman())) { + newPlots.push(plotToAdd); + } + } + handlePlotSelection(newPlots); }, - [plots, setTransferData], + [plots, selectedPlots, handlePlotSelection], ); - const selectAllPlots = useCallback(() => { - const plotIndexes = plots.map((plot) => plot.index.toHuman()); - handlePlotSelection(plotIndexes); - }, [plots, handlePlotSelection]); + // Slider change handler + const handlePodRangeChange = useCallback( + (value: number[]) => { + const newRange: [number, number] = [value[0], value[1]]; + setPodRange(newRange); + setTransferData(computeTransferData(selectedPlots, newRange)); + }, + [selectedPlots, setTransferData], + ); return ( - <> -
- -
-
- - - - +
+ {/* Pod Line Graph Visualization */} +
+ + + {/* Position in Line Display */} + {positionInfo && ( +
+

+ {positionInfo.start.toHuman("short")} - {positionInfo.end.toHuman("short")} +

+
+ )}
- + + {/* Total Pods Summary */} + {totalPods > 0 && ( +
+

Total Pods to send:

+

{formatter.noDec(amount)} Pods

+
+ )} + + {/* MultiSlider for pod range selection */} + {selectedPlots.length > 0 && ( +
+
+

Select Pods

+
+

{formatter.noDec(podRange[0])}

+
+ {totalPods > 0 && ( + + )} +
+

{formatter.noDec(podRange[1])}

+
+
+
+ )} +
); } diff --git a/src/pages/transfer/actions/pods/StepTwo.tsx b/src/pages/transfer/actions/pods/StepTwo.tsx index 1f8c07d78..86237f983 100644 --- a/src/pages/transfer/actions/pods/StepTwo.tsx +++ b/src/pages/transfer/actions/pods/StepTwo.tsx @@ -1,135 +1,19 @@ -import { TokenValue } from "@/classes/TokenValue"; import AddressInputField from "@/components/AddressInputField"; -import { ComboInputField } from "@/components/ComboInputField"; import PintoAssetTransferNotice from "@/components/PintoAssetTransferNotice"; -import PodRangeSelector from "@/components/PodRangeSelector"; import { Label } from "@/components/ui/Label"; -import { PODS } from "@/constants/internalTokens"; -import { useFarmerField } from "@/state/useFarmerField"; -import { Plot } from "@/utils/types"; import { AnimatePresence, motion } from "framer-motion"; -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; -import { PodTransferData } from "../TransferPods"; +import { Dispatch, SetStateAction } from "react"; interface StepTwoProps { - transferData: PodTransferData[]; - setTransferData: Dispatch>; destination: string | undefined; setDestination: Dispatch>; transferNotice: boolean; setTransferNotice: Dispatch>; } -export default function StepTwo({ - transferData, - setTransferData, - destination, - setDestination, - transferNotice, - setTransferNotice, -}: StepTwoProps) { - const { plots } = useFarmerField(); - const [selectedPlots, setSelectedPlots] = useState([]); - const [amount, setAmount] = useState("0"); - const [range, setRange] = useState<[TokenValue, TokenValue]>([TokenValue.ZERO, TokenValue.ZERO]); - - useEffect(() => { - const _newPlots: Plot[] = []; - for (const data of transferData) { - const _plot = plots.find((plot) => plot.index.eq(data.id)); - if (_plot) { - _newPlots.push(_plot); - } - } - setSelectedPlots(_newPlots); - }, []); - - useEffect(() => { - if (selectedPlots.length === 1) { - const plot = selectedPlots[0]; - setRange([plot.index, plot.index.add(plot.pods)]); - } - }, [selectedPlots]); - - const handleRangeChange = useCallback( - (newRange: TokenValue[]) => { - if (selectedPlots.length === 0 || selectedPlots.length > 1) return; - const plot = selectedPlots[0]; - const newStart = newRange[0]; - const newEnd = newRange[1]; - - const relativeStart = newStart.sub(plot.index); - const relativeEnd = newEnd.sub(plot.index); - - const newData = [ - { - id: plot.index, - start: relativeStart, - end: relativeEnd, - }, - ]; - - const newAmount = newEnd.sub(newStart).toHuman(); - - const batchUpdate = () => { - setRange([newStart, newEnd]); - setTransferData(newData); - setAmount(newAmount); - }; - batchUpdate(); - }, - [selectedPlots, setTransferData], - ); - - const handleAmountChange = useCallback( - (value: string) => { - const newAmount = value; - - if (selectedPlots.length === 0) return; - - const plot = selectedPlots[0]; - const amountValue = TokenValue.fromHuman(newAmount || "0", PODS.decimals); - const newEnd = plot.index.add(amountValue); - - const newStart = plot.index; - const relativeStart = newStart.sub(plot.index); - const relativeEnd = newEnd.sub(plot.index); - - const newData = [ - { - id: plot.index, - start: relativeStart, - end: relativeEnd, - }, - ]; - - const batchUpdate = () => { - setAmount(newAmount); - if (selectedPlots.length === 1) { - setRange([newStart, newEnd]); - setTransferData(newData); - } - }; - batchUpdate(); - }, - [selectedPlots, setTransferData], - ); - +export default function StepTwo({ destination, setDestination, transferNotice, setTransferNotice }: StepTwoProps) { return (
-
- - 1} - altText={selectedPlots.length > 1 ? "Balance:" : "Plot Balance:"} - /> -
@@ -148,11 +32,8 @@ export default function StepTwo({ /> )} - {" "} +
- {selectedPlots.length === 1 && ( - - )}
); } diff --git a/src/utils/podTransferUtils.ts b/src/utils/podTransferUtils.ts new file mode 100644 index 000000000..ebbff1758 --- /dev/null +++ b/src/utils/podTransferUtils.ts @@ -0,0 +1,98 @@ +import { TokenValue } from "@/classes/TokenValue"; +import { PODS } from "@/constants/internalTokens"; +import { PodTransferData } from "@/pages/transfer/actions/TransferPods"; +import { Plot } from "@/utils/types"; + +/** + * Converts a cumulative offset range [rangeStart, rangeEnd] across sorted plots + * into per-plot PodTransferData records with relative start/end values. + * + * Adapted from CreateListing's `listingData` useMemo logic. + * + * @param selectedPlots - Plots sorted by index + * @param podRange - [rangeStart, rangeEnd] cumulative offset (0 to totalPods) + * @returns PodTransferData[] with one entry per intersecting plot + */ +export function computeTransferData(selectedPlots: Plot[], podRange: [number, number]): PodTransferData[] { + const result: PodTransferData[] = []; + let cumulativeStart = 0; + + for (const plot of selectedPlots) { + const plotPods = plot.pods.toNumber(); + const cumulativeEnd = cumulativeStart + plotPods; + + // Check if this plot intersects with the selected range + if (podRange[1] > cumulativeStart && podRange[0] < cumulativeEnd) { + const startInPlot = Math.max(0, podRange[0] - cumulativeStart); + const endInPlot = Math.min(plotPods, podRange[1] - cumulativeStart); + + if (endInPlot > startInPlot) { + result.push({ + id: plot.index, + start: TokenValue.fromHuman(startInPlot, PODS.decimals), + end: TokenValue.fromHuman(endInPlot, PODS.decimals), + }); + } + } + + cumulativeStart = cumulativeEnd; + } + + return result; +} + +/** + * Converts a cumulative offset (relative to sorted plots) into an absolute + * TokenValue index on the pod line. + * + * Adapted from CreateListing's `selectedPodRange` useMemo logic. + * + * @param offset - Cumulative offset (0 to totalPods) + * @param sortedPlots - Plots sorted by index + * @returns Absolute TokenValue index on the pod line + */ +export function offsetToAbsoluteIndex(offset: number, sortedPlots: Plot[]): TokenValue { + let remainingOffset = offset; + + for (const plot of sortedPlots) { + const plotPods = plot.pods.toNumber(); + if (remainingOffset <= plotPods) { + return plot.index.add(TokenValue.fromHuman(remainingOffset, PODS.decimals)); + } + remainingOffset -= plotPods; + } + + // Fallback: offset exceeds total pods, clamp to end of last plot + const lastPlot = sortedPlots[sortedPlots.length - 1]; + return lastPlot.index.add(lastPlot.pods); +} + +/** + * Computes a consolidated summary range from transfer data records. + * + * - totalPods: sum of (end - start) across all records + * - placeInLineStart: first record's (id + start) - harvestableIndex + * - placeInLineEnd: last record's (id + end) - harvestableIndex + * + * @param transferData - Array of PodTransferData (must have at least one entry) + * @param harvestableIndex - Current harvestable index on the pod line + * @returns { totalPods, placeInLineStart, placeInLineEnd } + */ +export function computeSummaryRange( + transferData: PodTransferData[], + harvestableIndex: TokenValue, +): { totalPods: TokenValue; placeInLineStart: TokenValue; placeInLineEnd: TokenValue } { + const totalPods = transferData.reduce((sum, record) => sum.add(record.end.sub(record.start)), TokenValue.ZERO); + + const first = transferData[0]; + const last = transferData[transferData.length - 1]; + + const rangeStart = first.id.add(first.start); + const rangeEnd = last.id.add(last.end); + + return { + totalPods, + placeInLineStart: rangeStart.sub(harvestableIndex), + placeInLineEnd: rangeEnd.sub(harvestableIndex), + }; +}