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),
+ };
+}