Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions src/pages/transfer/actions/TransferPods.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -131,8 +125,6 @@ export default function TransferPods() {
<StepOne transferData={transferData} setTransferData={setTransferData} />
) : step === 2 ? (
<StepTwo
transferData={transferData}
setTransferData={setTransferData}
destination={destination}
setDestination={setDestination}
transferNotice={transferNotice}
Expand Down
52 changes: 30 additions & 22 deletions src/pages/transfer/actions/pods/FinalStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import AddressLink from "@/components/AddressLink";
import { Label } from "@/components/ui/Label";
import { useHarvestableIndex } from "@/state/useFieldData";
import { formatter } from "@/utils/format";
import { computeSummaryRange } from "@/utils/podTransferUtils";
import { useMemo } from "react";
import { PodTransferData } from "../TransferPods";

interface FinalStepProps {
Expand All @@ -13,36 +15,42 @@ interface FinalStepProps {
export default function FinalStep({ destination, transferData }: FinalStepProps) {
const harvestableIndex = useHarvestableIndex();

if (!destination || transferData.length === 0) {
const summary = useMemo(() => {
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 (
<div className="flex flex-col gap-6">
<div>
<Label className="font-[340] text-[1rem] sm:text-[1.25rem] mb-2">I'm sending</Label>
<div className="flex flex-col gap-4">
{transferData.map((transfer) => {
const placeInLine = transfer.id.sub(harvestableIndex);
const podAmount = transfer.end.sub(transfer.start);

return (
<div
key={`transfer_plot_${placeInLine.toHuman()}`}
className="pinto-h4 sm:pinto-h3 text-pinto-secondary sm:text-pinto-secondary flex flex-col gap-2 sm:flex-row sm:gap-1.5 place-self-end"
>
<div className="flex flex-row gap-1.5 items-center place-self-end sm:place-self-auto">
<span>{formatter.number(podAmount)}</span>
<img src={podIcon} className="h-8 w-8" alt="Plot" />
<span>Pods</span>
</div>
<div className="pinto-xs sm:pinto-h3 text-pinto-gray-4 sm:text-pinto-secondary flex flex-row gap-1.5 place-self-end sm:place-self-auto">
<span className={"text-pinto-gray-3"}>@</span>
<span>{formatter.number(placeInLine.add(transfer.start))} in Line</span>
</div>
</div>
);
})}
<div className="pinto-h4 sm:pinto-h3 text-pinto-secondary sm:text-pinto-secondary flex flex-col gap-2 sm:flex-row sm:gap-1.5 place-self-end">
<div className="flex flex-row gap-1.5 items-center place-self-end sm:place-self-auto">
<span>{formatter.number(totalPods)}</span>
<img src={podIcon} className="h-8 w-8" alt="Plot" />
<span>Pods</span>
</div>
<div className="pinto-xs sm:pinto-h3 text-pinto-gray-4 sm:text-pinto-secondary flex flex-row gap-1.5 place-self-end sm:place-self-auto">
{isSinglePlot ? (
<>
<span className="text-pinto-gray-3">@</span>
<span>{formatter.number(placeInLineStart)} in Line</span>
</>
) : (
<span>
between {formatter.number(placeInLineStart)} - {formatter.number(placeInLineEnd)} in Line
</span>
)}
</div>
</div>
</div>
</div>
<div>
Expand Down
223 changes: 159 additions & 64 deletions src/pages/transfer/actions/pods/StepOne.tsx
Original file line number Diff line number Diff line change
@@ -1,93 +1,188 @@
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 {
transferData: PodTransferData[];
setTransferData: Dispatch<SetStateAction<PodTransferData[]>>;
}

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<string[]>();
const { plots } = useFarmerField();
const harvestableIndex = useHarvestableIndex();

const [selectedPlots, setSelectedPlots] = useState<Plot[]>([]);
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 (
<>
<div className="flex flex-row justify-end -mt-[3.5rem] sm:-mt-[5rem]">
<Button
className={`font-[340] sm:pr-0 text-[1rem] sm:text-[1.25rem] text-pinto-green-4 bg-transparent hover:underline hover:bg-transparent`}
onClick={() => selectAllPlots()}
>
Select all Plots
</Button>
</div>
<div className="flex flex-col gap-4">
<Label>Which Plots do you want to send?</Label>
<ToggleGroup
type="multiple"
value={selected}
onValueChange={handlePlotSelection}
className="flex flex-col w-auto h-auto justify-between gap-2"
>
<PlotsTable selected={selected} useToggle />
</ToggleGroup>
<div className="flex flex-col gap-4">
{/* Pod Line Graph Visualization */}
<div className="flex flex-col gap-3">
<PodLineGraph
selectedPlotIndices={selectedPlotIndices}
selectedPodRange={selectedPodRange}
label="My Pods In Line"
onPlotGroupSelect={handlePlotGroupSelect}
className="h-24"
/>

{/* Position in Line Display */}
{positionInfo && (
<div className="flex justify-center">
<p className="pinto-body text-pinto-light">
{positionInfo.start.toHuman("short")} - {positionInfo.end.toHuman("short")}
</p>
</div>
)}
</div>
</>

{/* Total Pods Summary */}
{totalPods > 0 && (
<div className="flex justify-between items-center p-4 bg-pinto-gray-1 rounded-lg">
<p className="pinto-body text-pinto-light">Total Pods to send:</p>
<p className="pinto-body font-semibold">{formatter.noDec(amount)} Pods</p>
</div>
)}

{/* MultiSlider for pod range selection */}
{selectedPlots.length > 0 && (
<div className="flex flex-col gap-3 animate-fade-in">
<div className="flex items-center gap-4 w-full">
<p className="pinto-body text-pinto-light whitespace-nowrap">Select Pods</p>
<div className="flex items-center gap-3 flex-1 p-4">
<p className="pinto-body text-pinto-light w-[60px] text-right">{formatter.noDec(podRange[0])}</p>
<div className="flex-1">
{totalPods > 0 && (
<MultiSlider
value={podRange}
onValueChange={handlePodRangeChange}
step={1}
min={0}
max={totalPods}
className="w-full"
/>
)}
</div>
<p className="pinto-body text-pinto-light w-[60px] text-right">{formatter.noDec(podRange[1])}</p>
</div>
</div>
</div>
)}
</div>
);
}
Loading
Loading