aboutsummaryrefslogtreecommitdiff
path: root/jb-ui/src/components/Redemption.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'jb-ui/src/components/Redemption.tsx')
-rw-r--r--jb-ui/src/components/Redemption.tsx981
1 files changed, 964 insertions, 17 deletions
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 (
<>
<h2 className="text-xs font-mono font-bold p-0 m-0">
- {/* {cashout_methods.length} Redemption Methods */}
- 30+ Redemption Methods
+ {userCashoutMethods.length}
+ {userCashoutMethods.length != null ? "+" : ""} Redemption Methods
</h2>
</>
);
};
-const RedemptionModalBody = () => {
+interface CashoutMethodListItemDescriptionProps {
+ description?: string;
+}
+
+function CashoutMethodListItemDescription({
+ description,
+}: CashoutMethodListItemDescriptionProps) {
+ return (
+ <div
+ className="
+ text-[8px] text-left
+ bg-blue-600
+ max-h-24 overflow-y-auto
+ [&::-webkit-scrollbar]:h-1
+ [&::-webkit-scrollbar]:w-1
+ [&::-webkit-scrollbar-track]:bg-blue-100
+ [&::-webkit-scrollbar-thumb]:bg-blue-300
+ "
+ dangerouslySetInnerHTML={{ __html: description! }}
+ />
+ );
+}
+
+interface CashoutMethodListItemProps {
+ row: Row<CashoutMethodOut>;
+}
+
+interface CashoutMethodItemAmountProps {
+ cm: CashoutMethodOut;
+}
+
+const CashoutMethodItemAmount = ({ cm }: CashoutMethodItemAmountProps) => {
+ const cardRange = cm.min_value !== cm.max_value;
+
+ if (cardRange) {
+ return (
+ <>
+ <div className="label text-left">Min Amount:</div>
+ <div className="value text-right">
+ {formatCentsToUSD(cm.min_value_usd || 0)}
+ </div>
+
+ <div className="label text-left">Max Amount:</div>
+ <div className="value text-right">
+ {formatCentsToUSD(cm.max_value_usd || 0)}
+ </div>
+ </>
+ );
+ } else {
+ return (
+ <>
+ <div className="label text-left">Amount:</div>
+ <div className="value text-right">
+ {formatCentsToUSD(cm.min_value_usd || 0)}
+ </div>
+ </>
+ );
+ }
+};
+
+function CashoutMethodListItem({ row }: CashoutMethodListItemProps) {
+ const [showDescription, setShowDescription] = useState(false);
+
+ return (
+ <div
+ key={row.original.id}
+ className={clsx(
+ "transition-colors my-1",
+ row.getIsSelected() ? "border-red-500" : "border-blue-50",
+ )}
+ >
+ <div
+ className="cm-item-body grid grid-cols-2 px-2"
+ style={{
+ gap: 2,
+ padding: 2,
+ }}
+ >
+ <div>
+ <h2
+ className="cm-item-head text-xs font-semibold rounded flex justify-between
+ py-1 px-0"
+ >
+ {stripPriceFromTitle(row.original.name)}
+ </h2>
+ <div
+ className="grid grid-cols-[auto_1fr]
+ text-xs font-mono
+ px-2"
+ >
+ <CashoutMethodItemAmount cm={row.original} />
+ </div>
+
+ <button
+ className="cursor-pointer
+ text-blue-200 text-sm px-3 py-1/2
+
+ bg-blue-600
+ hover:bg-blue-700 hover:text-blue-50 hover:border-transparent
+ rounded
+ "
+ onClick={() => row.toggleSelected()}
+ >
+ Redeem
+ </button>
+ </div>
+
+ <div
+ className="group relative inline-flex items-center justify-center
+ p-3 rounded-sm
+ bg-gradient-from-center
+ transition-all duration-300
+ "
+ >
+ {/* animated glow blob behind the image */}
+ <div
+ className="
+ absolute inset-0 rounded-2xl
+ opacity-0 group-hover:opacity-20
+ blur-xl transition-opacity duration-300"
+ />
+
+ <img
+ src={row.original.image_url!}
+ alt={row.original.name}
+ style={{ height: 60 }}
+ className="relative z-10 drop-shadow-md
+ text-center align-middle
+ group-hover:drop-shadow-lg transition-all duration-300 group-hover:scale-105"
+ />
+ </div>
+ </div>
+
+ {/* Not every CashoutMethod has a Description */}
+ {row.original.description && (
+ <div
+ className="cm-item-bottom
+ m-2 px-2 py-1
+ bg-blue-600"
+ >
+ <p
+ className="
+ text-[8px] font-semibold
+ cursor-pointer text-right
+ hover:underline"
+ onClick={() => setShowDescription(!showDescription)}
+ >
+ Show Description
+ </p>
+
+ {showDescription && (
+ <CashoutMethodListItemDescription
+ description={row.original.description}
+ />
+ )}
+ </div>
+ )}
+ </div>
+ );
+}
+
+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<HTMLInputElement>) => {
+ 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 (
- <div className="grid grid-cols-[auto_1fr] text-xs">
- <div className="label text-center p-2">
- Paypal, Visa, Giftcards will be available. Finishing processing the
- transfer from Amazon.
+ <div className="px-4 ">
+ <label
+ className="
+ flex items-center
+ text-xs text-blue-50 border-b
+ "
+ htmlFor="amount-input"
+ >
+ Amount:{" "}
+ </label>
+
+ <div className="flex items-left gap-2 min-w-0">
+ <span>$</span>
+ <input
+ id="amount-input"
+ className="flex-1 min-w-0 w-full"
+ type="text"
+ inputMode="decimal"
+ value={displayValue}
+ onChange={handleChange}
+ placeholder="0.00"
+ aria-invalid={overLimit}
+ />
</div>
+
+ {!overLimit && (
+ <div className="text-[8px] text-emerald-50">
+ Max: ${(maxCents / 100).toFixed(2)}
+ </div>
+ )}
+
+ {overLimit && (
+ <div
+ role="alert"
+ className="
+ text-red-400 text-[8px]
+ font-semibold
+ "
+ >
+ Amount exceeds your available balance of $
+ {(maxCents / 100).toFixed(2)}.
+ </div>
+ )}
</div>
);
};
+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 (
+ <>
+ <div>
+ <h2 className="text-center cm-item-head text-sm font-semibold py-1">
+ {selected.type === "PAYPAL"
+ ? `${selected.name} (${selected.ext_id})`
+ : selected.name}
+ </h2>
+ </div>
+
+ <div
+ className="
+ cm-item-body grid grid-cols-2
+ border-b border-blue-900
+ py-2
+ "
+ >
+ <RedemptionDetailAmountEnter
+ reqAmount={reqAmount}
+ setRequestAmount={setRequestAmount}
+ maxCents={selected.max_value}
+ />
+
+ <div className="flex items-center ">
+ <button
+ className="
+ flex items-center
+ px-4 py-1 font-semibold text-white bg-blue-600
+ cursor-pointer
+ rounded-lg shadow-md hover:bg-blue-700"
+ onClick={submitCashOut}
+ >
+ Submit
+ </button>
+ </div>
+ </div>
+
+ {errorMsg && (
+ <div
+ className="
+ font-mono text-xs
+ py-1 bg-orange-700 text-red-200"
+ >
+ <h3>{errorMsg}</h3>
+ </div>
+ )}
+
+ <div className="">
+ <p
+ className="
+ text-[8px] white-blue-200 font-mono
+ px-6 py-1
+ "
+ >
+ You will recieve an email with confirmation deatils when requesting
+ the Cashout Method
+ </p>
+ </div>
+
+ <div
+ className="group relative inline-flex items-center justify-center
+ p-3 rounded-sm
+ bg-gradient-from-center
+ transition-all duration-300
+ "
+ >
+ {/* animated glow blob behind the image */}
+ <div
+ className="
+ absolute inset-0 rounded-2xl
+ opacity-0 group-hover:opacity-20
+ blur-xl transition-opacity duration-300"
+ />
+
+ <img
+ src={selected.image_url!}
+ alt={selected.name}
+ style={{ height: 120 }}
+ className="relative z-10 drop-shadow-md
+ text-center align-middle
+ group-hover:drop-shadow-lg transition-all duration-300 group-hover:scale-105"
+ />
+ </div>
+
+ <div
+ className="cm-item-bottom
+ m-2 px-2 py-1
+ bg-blue-600"
+ >
+ <CashoutMethodListItemDescription description={selected.description} />
+ </div>
+ </>
+ );
+};
+
+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<RowSelectionState>({});
+ 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<CashoutMethodOut>();
+ 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<CashoutMethodOut> = (
+ 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 (
+ <div>
+ <div
+ className="
+ flex justify-center items-center
+ gap-2 my-2"
+ >
+ <div
+ className={clsx(
+ "rounded-md",
+ "bg-blue-600 hover:bg-blue-700",
+ "py-0.5 px-2.5",
+ "font-mono text-[8px]",
+ "text-white cursor-pointer",
+ globalFilter == "CASH" ? "bg-blue-800" : "bg-blue-600",
+ )}
+ onClick={() => setGlobalFilter("CASH")}
+ >
+ Cash
+ </div>
+
+ <div
+ className="
+ rounded-md
+ bg-blue-700 hover:bg-blue-800
+ py-0.5 px-2
+ font-mono text-[8px]
+ text-white cursor-not-allowed"
+ // onClick={() => setGlobalFilter("GIFT_CARD")}
+ >
+ Gift Card
+ </div>
+ </div>
+
+ <div
+ className="
+ max-h-96 overflow-y-auto
+ [&::-webkit-scrollbar]:h-1
+ [&::-webkit-scrollbar]:w-1
+ [&::-webkit-scrollbar-track]:bg-blue-100
+ [&::-webkit-scrollbar-thumb]:bg-blue-300
+ "
+ >
+ {table.getRowModel().rows.map((row) => (
+ <CashoutMethodListItem
+ key={row.original.id}
+ row={row}
+ // setRowSelection={setRowSelection}
+ />
+ ))}
+ </div>
+
+ <div
+ className="
+ px-2 py-1
+ flex items-center justify-between text-xs"
+ >
+ {/* Page info */}
+ <div className="text-blue-50">
+ Page {table.getState().pagination.pageIndex + 1} of{" "}
+ {table.getPageCount()}
+ </div>
+
+ {/* Navigation buttons */}
+ <div className="flex gap-8 text-xs">
+ <button
+ onClick={() => table.previousPage()}
+ disabled={!table.getCanPreviousPage()}
+ className="px-3 py-1 border border-blue-200 rounded
+ font-variant-emoji-text
+ hover:cursor-pointer hover:bg-blue-700
+ disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ {"⬅\uFE0E"}
+ </button>
+ <button
+ onClick={() => table.nextPage()}
+ disabled={!table.getCanNextPage()}
+ className="px-3 py-1 border border-blue-200 rounded
+ font-variant-emoji-text
+ hover:cursor-pointer hover:bg-blue-700
+ disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ {"⮕\uFE0E"}
+ </button>
+ </div>
+
+ {/* Page size selector */}
+ <select
+ value={table.getState().pagination.pageSize}
+ onChange={(e) => table.setPageSize(Number(e.target.value))}
+ className="px-2 py-1 border border-blue-200 rounded
+ hover:cursor-pointer hover:bg-blue-700"
+ >
+ {[10, 20, 30, 40].map((pageSize) => (
+ <option key={pageSize} value={pageSize}>
+ Show {pageSize}
+ </option>
+ ))}
+ </select>
+ </div>
+ </div>
+ );
+};
+
+const RedemptionModalSteps = () => {
+ const dispatch = useAppDispatch();
+ const selected = useAppSelector((state) => state.app.cmSelected);
+ const showHistory = useAppSelector((state) => state.app.cmShowHistory);
+
+ return (
+ <div
+ className="
+ flex justify-center items-center
+ border-b border-blue-700
+ "
+ >
+ <ol
+ className="
+ flex items-center w-1/2
+ my-1
+ "
+ >
+ <li
+ className={clsx(
+ "flex w-full items-center cursor-pointer",
+ "hover:text-blue-50",
+ !selected ? "text-white" : "text-blue-200",
+ )}
+ onClick={() => {
+ dispatch(setCmSelected(undefined));
+ dispatch(setCmShowHistory(false));
+ }}
+ >
+ <span
+ className="
+ flex items-center justify-center
+ w-6 h-6
+ "
+ >
+ <List />
+ </span>
+ </li>
+
+ <li
+ className={clsx(
+ "flex w-full items-center",
+ selected ? "text-white" : "text-blue-400 cursor-not-allowed",
+ )}
+ >
+ <span
+ className="
+ flex items-center justify-center
+ w-6 h-6
+ "
+ >
+ <IdCard />
+ </span>
+ </li>
+
+ <li
+ className={clsx(
+ "flex items-center",
+ showHistory ? "text-white" : "text-blue-400",
+ )}
+ onClick={() => {
+ dispatch(setCmSelected(undefined));
+ dispatch(setCmShowHistory(true));
+ }}
+ >
+ <span
+ className="
+ flex items-center justify-center
+ w-6 h-6
+ ml-8
+ hover:text-white cursor-pointer
+ "
+ >
+ <PackageCheck />
+ </span>
+ </li>
+ </ol>
+ </div>
+ );
+};
+
+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<RowSelectionState>({});
+ const [globalFilter, setGlobalFilter] = useState("");
+ // const [pagination, setPagination] = useState({
+ // pageIndex: 0,
+ // pageSize: 10,
+ // });
+
+ const columnHelper = createColumnHelper<CashoutRequestSummary>();
+ 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 (
+ <div>
+ <div
+ className="
+ flex justify-center items-center
+ gap-2 my-2"
+ >
+ {STATUSES.map((status) => {
+ const isActive = globalFilter === status;
+
+ return (
+ <div
+ key={status}
+ className={clsx(
+ "rounded-md",
+ "py-0.5 px-2.5",
+ "font-mono text-[8px]",
+ "text-white cursor-pointer",
+ isActive ? "bg-blue-800" : "bg-blue-600 hover:bg-blue-700",
+ )}
+ onClick={() => setGlobalFilter(status)}
+ >
+ {status}
+ </div>
+ );
+ })}
+ </div>
+
+ <div className="w-full border-t border-blue-800 p-2">
+ <table className="text-xs table-fixed border-collapse">
+ <thead className="font-bold">
+ {table.getHeaderGroups().map((headerGroup) => (
+ <tr key={headerGroup.id}>
+ {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 (
+ <th
+ key={header.id}
+ className={`p-0 m-0 ${alignClass}`}
+ style={{
+ width: `${header.column.getSize()}px`,
+ }}
+ >
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+ </th>
+ );
+ })}
+ </tr>
+ ))}
+ </thead>
+ <tbody>
+ {table.getRowModel().rows.map((row) => (
+ <tr key={row.id}>
+ {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 (
+ <td
+ key={cell.id}
+ className={`p-0 ${alignClass}`}
+ style={{
+ width: `${cell.column.getSize()}px`,
+ }}
+ >
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext(),
+ )}
+ </td>
+ );
+ })}
+ </tr>
+ ))}
+ </tbody>
+
+ <tfoot>
+ <tr>
+ <td colSpan={columns.length} className="p-2">
+ <div className="flex items-center justify-between text-xs">
+ {/* Page info */}
+ <div className="text-blue-50">
+ Page {table.getState().pagination.pageIndex + 1} of{" "}
+ {table.getPageCount()}
+ </div>
+
+ {/* Navigation buttons */}
+ <div className="flex gap-8 text-xs">
+ <button
+ onClick={() => table.previousPage()}
+ disabled={!table.getCanPreviousPage()}
+ className="px-3 py-1 border border-blue-200 rounded
+ font-variant-emoji-text
+ hover:cursor-pointer hover:bg-blue-700
+ disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ {"⬅\uFE0E"}
+ </button>
+ <button
+ onClick={() => table.nextPage()}
+ disabled={!table.getCanNextPage()}
+ className="px-3 py-1 border border-blue-200 rounded
+ font-variant-emoji-text
+ hover:cursor-pointer hover:bg-blue-700
+ disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ {"⮕\uFE0E"}
+ </button>
+ </div>
+
+ {/* Page size selector */}
+ <select
+ value={table.getState().pagination.pageSize}
+ onChange={(e) => table.setPageSize(Number(e.target.value))}
+ className="px-2 py-1 border border-blue-200 rounded
+ hover:cursor-pointer hover:bg-blue-700"
+ >
+ {[10, 20, 30, 40].map((pageSize) => (
+ <option key={pageSize} value={pageSize}>
+ Show {pageSize}
+ </option>
+ ))}
+ </select>
+ </div>
+ </td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ </div>
+ );
+};
+
+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 (
+ <>
+ <RedemptionModalSteps />
+ <RedemptionHistory />
+ </>
+ );
+ }
+
+ if (selected) {
+ return (
+ <>
+ <RedemptionModalSteps />
+ <RedemptionDetails />
+ </>
+ );
+ } else {
+ return (
+ <>
+ <RedemptionModalSteps />
+ <RedemptionBrowser />
+ </>
+ );
+ }
+};
+
const Redemption = () => {
- const [showRedemption, setShowRedemption] = useState(false);
+ const [showRedemption, setShowRedemption] = useState(true);
+
+ const { data: user } = useGetMeQuery();
+ if (!user?.product_user_id) return null;
return (
<div
className={clsx(
"fixed rounded bg-blue-500 text-white shadow-lg z-60 \
- top-1 sm:top-6 \
+ top-2 \
right-1 sm:right-2",
- showRedemption ? "w-[98vw] sm:w-[400px]" : "w-[220px]",
+ showRedemption ? "w-[98vw] sm:w-[320px]" : "w-[220px]",
)}
>
<div
@@ -47,9 +995,8 @@ const Redemption = () => {
>
<RedemptionHeader />
- <h4 className="text-[10px] text-center italic text-white-300 py-0">
- {/* (Click to {showRedemption ? "hide" : "show"}) */}
- Details within 24hr.
+ <h4 className="text-[10px] text-center italic text-blue-300 py-0">
+ (Click to {showRedemption ? "hide" : "show"})
</h4>
</div>