From cd173e67e4e0514271371f4a5e5136f7f61d06d1 Mon Sep 17 00:00:00 2001 From: Max Nanis Date: Tue, 29 Sep 2026 11:47:48 -0700 Subject: Redemption Modal. Redemption Magic flow. User Account Icon --- jb-ui/src/components/Redemption.tsx | 981 +++++++++++++++++++++++++++++++++++- 1 file changed, 964 insertions(+), 17 deletions(-) (limited to 'jb-ui/src/components/Redemption.tsx') diff --git a/jb-ui/src/components/Redemption.tsx b/jb-ui/src/components/Redemption.tsx index 1d76f17..2d0380e 100644 --- a/jb-ui/src/components/Redemption.tsx +++ b/jb-ui/src/components/Redemption.tsx @@ -1,42 +1,990 @@ import { clsx } from "clsx"; -import { useState } from "react"; -// import { useSelector } from "react-redux"; -// import { cashoutMethodsSelectors } from "@/models/cashoutMethodsSlice"; +import { + bpid, + formatCentsToUSD, + truncate, + stripPriceFromTitle, +} from "@/lib/utils"; +import moment from "moment"; +import { useAppDispatch, useAppSelector } from "@/hooks"; +import { useState, useEffect } from "react"; +import { useGetMeQuery } from "@/models/authAPI"; +import { + CashoutMethodOut, + CashoutMethodsResponse, + WalletApi, + CashoutRequestResponse, + CashoutRequestSummary, +} from "@/api_fsb"; +import { WalletApi as JBApi, CashoutRequest } from "@/api_jb"; +import { List, IdCard, PackageCheck } from "lucide-react"; +import { + addCashoutMethods, + selectAllCashoutMethods, +} from "@/models/cashoutMethodsSlice"; +import { setCmSelected, setCmShowHistory } from "@/models/appSlice"; +import { + createColumnHelper, + getCoreRowModel, + getPaginationRowModel, + useReactTable, + getFilteredRowModel, + getSortedRowModel, + flexRender, + RowSelectionState, + FilterFn, +} from "@tanstack/react-table"; +import { useSelector } from "react-redux"; +import type { Row } from "@tanstack/react-table"; + +import { useGetUserCashoutsQuery } from "@/models/cashoutRequests"; const RedemptionHeader = () => { - // const cashout_methods = useSelector(cashoutMethodsSelectors.selectAll); + const userCashoutMethods = useSelector(selectAllCashoutMethods) || []; return ( <>

- {/* {cashout_methods.length} Redemption Methods */} - 30+ Redemption Methods + {userCashoutMethods.length} + {userCashoutMethods.length != null ? "+" : ""} Redemption Methods

); }; -const RedemptionModalBody = () => { +interface CashoutMethodListItemDescriptionProps { + description?: string; +} + +function CashoutMethodListItemDescription({ + description, +}: CashoutMethodListItemDescriptionProps) { + return ( +
+ ); +} + +interface CashoutMethodListItemProps { + row: Row; +} + +interface CashoutMethodItemAmountProps { + cm: CashoutMethodOut; +} + +const CashoutMethodItemAmount = ({ cm }: CashoutMethodItemAmountProps) => { + const cardRange = cm.min_value !== cm.max_value; + + if (cardRange) { + return ( + <> +
Min Amount:
+
+ {formatCentsToUSD(cm.min_value_usd || 0)} +
+ +
Max Amount:
+
+ {formatCentsToUSD(cm.max_value_usd || 0)} +
+ + ); + } else { + return ( + <> +
Amount:
+
+ {formatCentsToUSD(cm.min_value_usd || 0)} +
+ + ); + } +}; + +function CashoutMethodListItem({ row }: CashoutMethodListItemProps) { + const [showDescription, setShowDescription] = useState(false); + + return ( +
+
+
+

+ {stripPriceFromTitle(row.original.name)} +

+
+ +
+ + +
+ +
+ {/* animated glow blob behind the image */} +
+ + {row.original.name} +
+
+ + {/* Not every CashoutMethod has a Description */} + {row.original.description && ( +
+

setShowDescription(!showDescription)} + > + Show Description +

+ + {showDescription && ( + + )} +
+ )} +
+ ); +} + +interface CashoutAmountInputProps { + reqAmount: number; // cents + setRequestAmount: (cents: number) => void; + maxCents: number; // max withdrawable, in cents +} + +const RedemptionDetailAmountEnter = ({ + reqAmount, + setRequestAmount, + maxCents, +}: CashoutAmountInputProps) => { + // Local text state so users can type freely (e.g. "12.", "0.5") + // without fighting a reformatted value every keystroke. + const [displayValue, setDisplayValue] = useState( + reqAmount ? (reqAmount / 100).toString() : "", + ); + + const overLimit = reqAmount > maxCents; + + const handleChange = (e: React.ChangeEvent) => { + const raw = e.target.value; + + // Allow only digits and a single decimal point with up to 2 decimals + if (!/^\d*\.?\d{0,2}$/.test(raw)) return; + + setDisplayValue(raw); + + const dollars = parseFloat(raw); + setRequestAmount(isNaN(dollars) ? 0 : Math.round(dollars * 100)); + }; + return ( -
-
- Paypal, Visa, Giftcards will be available. Finishing processing the - transfer from Amazon. +
+ + +
+ $ +
+ + {!overLimit && ( +
+ Max: ${(maxCents / 100).toFixed(2)} +
+ )} + + {overLimit && ( +
+ Amount exceeds your available balance of $ + {(maxCents / 100).toFixed(2)}. +
+ )}
); }; +const RedemptionDetails = () => { + // res = session.post(JB_BASE_URL + 'wallet/cashout/request/', json={ + // 'amount': 10_00, + // 'cashout_method_id': paypal_cm['id'], + // }) + // res.raise_for_status() + // print(res.json()) + const selected = useAppSelector((state) => state.app.cmSelected); + if (!selected) return null; + const dispatch = useAppDispatch(); + + const [reqAmount, setRequestAmount] = useState(selected.min_value); + const [errorMsg, setErrorMesg] = useState(""); + + const { data: user } = useGetMeQuery(); + if (!user?.product_user_id) return; + + const submitCashOut = () => { + const ccr = { + cashout_method_id: selected.id, + amount: reqAmount, + } as CashoutRequest; + + // const config = new Configuration({ basePath: "http://localhost:5174" }); + new JBApi() + .requestCashoutWalletCashoutRequestPost(ccr) + .then(() => { + // TODO: will `res.data.cashout_method` & `res.data.confirmation_link` + // TODO: always be available in this response? + dispatch(setCmSelected(undefined)); + dispatch(setCmShowHistory(true)); + }) + .catch((err) => { + console.log("ERR", err); + const response = err?.response?.data as + | CashoutRequestResponse + | undefined; + setErrorMesg(response?.msg ?? err.message); + }); + }; + + return ( + <> +
+

+ {selected.type === "PAYPAL" + ? `${selected.name} (${selected.ext_id})` + : selected.name} +

+
+ +
+ + +
+ +
+
+ + {errorMsg && ( +
+

{errorMsg}

+
+ )} + +
+

+ You will recieve an email with confirmation deatils when requesting + the Cashout Method +

+
+ +
+ {/* animated glow blob behind the image */} +
+ + {selected.name} +
+ +
+ +
+ + ); +}; + +const RedemptionBrowser = () => { + // Don't show the browser if a CashoutMethod is already selected + const selected = useAppSelector((state) => state.app.cmSelected); + if (selected) return; + + const dispatch = useAppDispatch(); + const userCashoutMethods = useSelector(selectAllCashoutMethods); + + const [rowSelection, setRowSelection] = useState({}); + const [globalFilter, setGlobalFilter] = useState("CASH"); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + }); + + const { data: user } = useGetMeQuery(); + + useEffect(() => { + if (!user?.product_user_id) return; + + new WalletApi() + .getCashoutMethodsProductIdCashoutMethodsGet(bpid, user.product_user_id) + .then((res) => { + const response = res.data as CashoutMethodsResponse; + dispatch(addCashoutMethods(response.cashout_methods)); + }); + }, [bpid, user?.product_user_id]); + + const columnHelper = createColumnHelper(); + const columns = [ + columnHelper.accessor("name", { + header: () => "Name", + }), + columnHelper.accessor("image_url", { + header: () => "Image", + }), + columnHelper.accessor("description", { + header: () => "Desc", + }), + ]; + + useEffect(() => { + const selectedRows = table + .getSelectedRowModel() + .rows.map((r) => r.original); + const cm = selectedRows[0] as CashoutMethodOut; + dispatch(setCmSelected(cm)); + }, [rowSelection]); + + const categoryFilter: FilterFn = ( + row, + _columnId, + filterValue, + ) => row.original.category === filterValue; + + const table = useReactTable({ + data: userCashoutMethods || [], + columns: columns, + getCoreRowModel: getCoreRowModel(), + getRowId: (row) => row.id, + + enableRowSelection: true, + enableMultiRowSelection: false, + + state: { + pagination, + rowSelection, + globalFilter, + }, + + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + globalFilterFn: categoryFilter, + onGlobalFilterChange: setGlobalFilter, + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + }); + + return ( +
+
+
setGlobalFilter("CASH")} + > + Cash +
+ +
setGlobalFilter("GIFT_CARD")} + > + Gift Card +
+
+ +
+ {table.getRowModel().rows.map((row) => ( + + ))} +
+ +
+ {/* Page info */} +
+ Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} +
+ + {/* Navigation buttons */} +
+ + +
+ + {/* Page size selector */} + +
+
+ ); +}; + +const RedemptionModalSteps = () => { + const dispatch = useAppDispatch(); + const selected = useAppSelector((state) => state.app.cmSelected); + const showHistory = useAppSelector((state) => state.app.cmShowHistory); + + return ( +
+
    +
  1. { + dispatch(setCmSelected(undefined)); + dispatch(setCmShowHistory(false)); + }} + > + + + +
  2. + +
  3. + + + +
  4. + +
  5. { + dispatch(setCmSelected(undefined)); + dispatch(setCmShowHistory(true)); + }} + > + + + +
  6. +
+
+ ); +}; + +const RedemptionHistory = () => { + const { data: user } = useGetMeQuery(); + if (!user?.product_user_id) return; + + const { data: cashout_requests } = useGetUserCashoutsQuery( + { bpid, productUserId: user.product_user_id }, + { skip: !user.product_user_id }, + ); + + // --- Table work now --- + + // const [rowSelection, setRowSelection] = useState({}); + const [globalFilter, setGlobalFilter] = useState(""); + // const [pagination, setPagination] = useState({ + // pageIndex: 0, + // pageSize: 10, + // }); + + const columnHelper = createColumnHelper(); + const columns = [ + columnHelper.accessor("created", { + header: () => "Created", + cell: (info) => moment(info.getValue()).format("M/D/YY"), + size: 60, + meta: { + align: "left", + }, + }), + columnHelper.accessor("description", { + header: () => "Description", + cell: (info) => truncate(info.getValue(), 12), + size: 100, + meta: { + align: "left", + }, + }), + columnHelper.accessor("amount", { + header: () => "Amount", + cell: (info) => formatCentsToUSD(info.getValue()), + size: 80, + meta: { + align: "center", + }, + }), + columnHelper.accessor("status", { + header: () => "Status", + cell: (props) => { + const desc = props.getValue(); + switch (desc) { + case "PENDING": + return "Pending 🤞"; + + case "APPROVED": + return "Approved 👍"; + + case "REJECTED": + return "Rejected 🚫"; + + case "CANCELLED": + return "Cancelled 🚫"; + + case "FAILED": + return "Failed ❌"; + + case "COMPLETE": + return "Complete ✅"; + + default: + return desc; + } + }, + size: 80, + meta: { + align: "left", + }, + }), + ]; + + let table = useReactTable({ + data: cashout_requests || [], + columns: columns, + getCoreRowModel: getCoreRowModel(), + getRowId: (row) => row.id, + + // enableRowSelection: true, + // enableMultiRowSelection: false, + state: { + // pagination, + // rowSelection, + globalFilter, + }, + // onPaginationChange: setPagination, + // onRowSelectionChange: setRowSelection, + onGlobalFilterChange: setGlobalFilter, + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + }); + const STATUSES = ["PENDING", "APPROVED", "REJECTED", "COMPLETE"]; + + return ( +
+
+ {STATUSES.map((status) => { + const isActive = globalFilter === status; + + return ( +
setGlobalFilter(status)} + > + {status} +
+ ); + })} +
+ +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const align = header.column.columnDef.meta?.align || "left"; + const alignClass = + align === "center" + ? "text-center" + : align === "right" + ? "text-right" + : "text-left"; + + return ( + + ); + })} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => { + const align = cell.column.columnDef.meta?.align || "left"; + const alignClass = + align === "center" + ? "text-center" + : align === "right" + ? "text-right" + : "text-left"; + + return ( + + ); + })} + + ))} + + + + + + + +
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} +
+
+ {/* Page info */} +
+ Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} +
+ + {/* Navigation buttons */} +
+ + +
+ + {/* Page size selector */} + +
+
+
+
+ ); +}; + +const RedemptionModalBody = () => { + // Show (1) Selection, (2) Amount Application, (3) Confirmation + const selected = useAppSelector((state) => state.app.cmSelected); + const showHistory = useAppSelector((state) => state.app.cmShowHistory); + + if (showHistory) { + return ( + <> + + + + ); + } + + if (selected) { + return ( + <> + + + + ); + } else { + return ( + <> + + + + ); + } +}; + const Redemption = () => { - const [showRedemption, setShowRedemption] = useState(false); + const [showRedemption, setShowRedemption] = useState(true); + + const { data: user } = useGetMeQuery(); + if (!user?.product_user_id) return null; return (
{ > -

- {/* (Click to {showRedemption ? "hide" : "show"}) */} - Details within 24hr. +

+ (Click to {showRedemption ? "hide" : "show"})

-- cgit v1.2.3