From ed201936813d9a720b51e6cb40a05267b94e7314 Mon Sep 17 00:00:00 2001 From: Max Nanis Date: Thu, 10 Sep 2026 17:46:41 -0700 Subject: Typescript prettier Committing seperate so it's easier to see actual changes. --- jb-ui/src/components/EventMarquee.tsx | 428 +++++----- jb-ui/src/components/Footer.tsx | 76 +- jb-ui/src/components/Profiling.tsx | 1297 ++++++++++++++++--------------- jb-ui/src/components/Wallet.tsx | 859 ++++++++++---------- jb-ui/src/components/ui/button.tsx | 20 +- jb-ui/src/components/ui/checkbox.tsx | 14 +- jb-ui/src/components/ui/confetti.tsx | 98 +-- jb-ui/src/components/ui/dot-pattern.tsx | 60 +- jb-ui/src/components/ui/field.tsx | 72 +- jb-ui/src/components/ui/label.tsx | 12 +- jb-ui/src/components/ui/magic-card.tsx | 70 +- jb-ui/src/components/ui/pagination.tsx | 30 +- jb-ui/src/components/ui/separator.tsx | 14 +- jb-ui/src/components/ui/skeleton.tsx | 6 +- 14 files changed, 1571 insertions(+), 1485 deletions(-) (limited to 'jb-ui/src/components') diff --git a/jb-ui/src/components/EventMarquee.tsx b/jb-ui/src/components/EventMarquee.tsx index 4ce4f15..6aab3f8 100644 --- a/jb-ui/src/components/EventMarquee.tsx +++ b/jb-ui/src/components/EventMarquee.tsx @@ -1,230 +1,250 @@ -import { EventEnvelope, EventType, TaskEnterPayload, TaskFinishPayload } from "@/api_fsb"; -import { formatSecondsVerbose, formatSource, formatStatus, truncate } from "@/lib/utils"; +import { + EventEnvelope, + EventType, + TaskEnterPayload, + TaskFinishPayload, +} from "@/api_fsb"; +import { + formatSecondsVerbose, + formatSource, + formatStatus, + truncate, +} from "@/lib/utils"; import { selectAllEvents, selectCurrentSpeed } from "@/models/grlEventsSlice"; -import getUnicodeFlagIcon from 'country-flag-icons/unicode'; +import getUnicodeFlagIcon from "country-flag-icons/unicode"; import { useEffect, useRef, useState } from "react"; import { useSelector } from "react-redux"; // ─── Config ─────────────────────────────────────────────────────────────────── -const SPEED_EASING = 0.025; // how quickly speed transitions (per frame) +const SPEED_EASING = 0.025; // how quickly speed transitions (per frame) interface EventItemProps { - event: EventEnvelope; + event: EventEnvelope; } const EventTaskEnter = ({ event }: EventItemProps) => { - const payload = event.payload as TaskEnterPayload; - - return ( - <> - {getUnicodeFlagIcon(payload.country_iso)} -

- {truncate(event.product_user_id!, 6, "*****")} Entered{" "} - {formatSource(payload.source)}{" "} - (#{truncate(payload.survey_id!, 5, "***")}) -

- - ) -} + const payload = event.payload as TaskEnterPayload; + + return ( + <> + + {getUnicodeFlagIcon(payload.country_iso)} + +

+ {truncate(event.product_user_id!, 6, "*****")} Entered{" "} + {formatSource(payload.source)} (# + {truncate(payload.survey_id!, 5, "***")}) +

+ + ); +}; const EventTaskFinish = ({ event }: EventItemProps) => { - const payload = event.payload as TaskFinishPayload; - - return ( - <> - {getUnicodeFlagIcon(event.payload.country_iso)} -

- {truncate(event.product_user_id!, 6, "*****")}{" "} - {formatStatus(payload.status)} {formatSource(payload.source)}{" "} - (#{truncate(payload.survey_id!, 5, "***")}) in{" "} - {formatSecondsVerbose(payload.duration_sec!)} -

- - ) -} + const payload = event.payload as TaskFinishPayload; + + return ( + <> + + {getUnicodeFlagIcon(event.payload.country_iso)} + +

+ {truncate(event.product_user_id!, 6, "*****")}{" "} + {formatStatus(payload.status)} {formatSource(payload.source)} (# + {truncate(payload.survey_id!, 5, "***")}) in{" "} + {formatSecondsVerbose(payload.duration_sec!)} +

+ + ); +}; const EventUserCreated = ({ event }: EventItemProps) => { - - return ( - <> - {getUnicodeFlagIcon(event.payload.country_iso)} -

{truncate(event.product_user_id!, 6, "*****")} Created

- - ) -} - + return ( + <> + + {getUnicodeFlagIcon(event.payload.country_iso)} + +

{truncate(event.product_user_id!, 6, "*****")} Created

+ + ); +}; const EventComponent = ({ event }: EventItemProps) => { - - const renderContent = () => { - switch (event.event_type) { - case EventType.TaskEnter: - return - case EventType.TaskFinish: - return - case EventType.UserCreated: - return - default: - return <>

Unknown event

- } + const renderContent = () => { + switch (event.event_type) { + case EventType.TaskEnter: + return ; + case EventType.TaskFinish: + return ; + case EventType.UserCreated: + return ; + default: + return ( + <> +

Unknown event

+ + ); } + }; - return ( -
- {renderContent()} - - {/* ◆ */} -
- ) -} + return ( +
+ {renderContent()} + {/* ◆ */} +
+ ); +}; // ─── Main Component ─────────────────────────────────────────────────────────── export default function NewsTicker() { - // --- Redux --- - const reduxItems = useSelector(selectAllEvents); - const targetSpeedRedux = useSelector(selectCurrentSpeed); // px/sec - - // --- Scroll state (all refs — no re-renders from animation) --- - const containerRef = useRef(null); // The ticker band - const stripRef = useRef(null); // Scrolling strip - const translateX = useRef(0); - const currentSpeed = useRef(targetSpeedRedux); - const targetSpeed = useRef(targetSpeedRedux); - - // Only state and refs persist across renders, the rest are re-initialized, - // that is why this isn't just a standard variable. - // Request Animation Frame reference (stores the animation frame ID) - const rafRef = useRef(null); - const lastTime = useRef(null); - - // --- Item tracking --- - // Track which IDs are already in the scroller to only append - // genuinely new ones. - const seenUUIDs = useRef(new Set()); - const pendingQueue = useRef([]); - const [scrollItems, setScrollItems] = useState([]); - - // --- Sync Redux → pendingQueue (only new items) --- - useEffect(() => { - const newItems = reduxItems.filter((item) => !seenUUIDs.current.has(item.event_uuid!)); - if (newItems.length === 0) return; - - newItems.forEach(item => seenUUIDs.current.add(item.event_uuid!)); - pendingQueue.current.push(...newItems); - }, [reduxItems]); - - // --- Sync target speed from Redux --- - useEffect(() => { - targetSpeed.current = targetSpeedRedux; - }, [targetSpeedRedux]); - - // --- RAF loop --- - useEffect(() => { - // This timestamp is a "high-resolution timestamp" provided by - // the browser, which is more accurate for measuring frame intervals - // than Date.now() - const loop = (timestamp: number) => { - console.log("RAF loop, timestamp:", timestamp, lastTime.current); - - if (!lastTime.current) lastTime.current = timestamp; - const delta = (timestamp - lastTime.current) / 1000; - lastTime.current = timestamp; - - // Slow easement towards targetSpeed - currentSpeed.current += - (targetSpeed.current - currentSpeed.current) * SPEED_EASING; - - // flush pending queue into React state - if (pendingQueue.current.length > 0) { - const incoming = pendingQueue.current.splice(0); - setScrollItems((prev) => [...prev, ...incoming]); - } - - // Advance strip, slides it over left by X pixels - translateX.current -= currentSpeed.current * delta; - if (stripRef.current) { - stripRef.current.style.transform = `translateX(${translateX.current}px)`; - } - - // prune items that have fully exited the left edge - if (stripRef.current && containerRef.current) { - const children = Array.from(stripRef.current.children); - const containerLeft = containerRef.current.getBoundingClientRect().left; - let pruneCount = 0; - let removedWidth = 0; - - for (const child of children) { - const rect = child.getBoundingClientRect(); - if (rect.right < containerLeft - 20) { - pruneCount++; - removedWidth += rect.width; - } else { - break; - } - } - - if (pruneCount > 0) { - translateX.current += removedWidth; - setScrollItems((prev) => prev.slice(pruneCount)); - } - } - - // Assign the next loop - rafRef.current = requestAnimationFrame(loop); - }; - - rafRef.current = requestAnimationFrame(loop); - return () => cancelAnimationFrame(rafRef.current!); - }, []); - - return ( -
- - {/* Ticker wrapper */} -
- - {/* The ticker band */} -
- - {/* Scrolling strip */} -
- {scrollItems.map(item => ( - - ))} - - {/* Trailing spacer so new content doesn't immediately snap in */} - - -
-
- -
+ // --- Redux --- + const reduxItems = useSelector(selectAllEvents); + const targetSpeedRedux = useSelector(selectCurrentSpeed); // px/sec + + // --- Scroll state (all refs — no re-renders from animation) --- + const containerRef = useRef(null); // The ticker band + const stripRef = useRef(null); // Scrolling strip + const translateX = useRef(0); + const currentSpeed = useRef(targetSpeedRedux); + const targetSpeed = useRef(targetSpeedRedux); + + // Only state and refs persist across renders, the rest are re-initialized, + // that is why this isn't just a standard variable. + // Request Animation Frame reference (stores the animation frame ID) + const rafRef = useRef(null); + const lastTime = useRef(null); + + // --- Item tracking --- + // Track which IDs are already in the scroller to only append + // genuinely new ones. + const seenUUIDs = useRef(new Set()); + const pendingQueue = useRef([]); + const [scrollItems, setScrollItems] = useState([]); + + // --- Sync Redux → pendingQueue (only new items) --- + useEffect(() => { + const newItems = reduxItems.filter( + (item) => !seenUUIDs.current.has(item.event_uuid!), + ); + if (newItems.length === 0) return; + + newItems.forEach((item) => seenUUIDs.current.add(item.event_uuid!)); + pendingQueue.current.push(...newItems); + }, [reduxItems]); + + // --- Sync target speed from Redux --- + useEffect(() => { + targetSpeed.current = targetSpeedRedux; + }, [targetSpeedRedux]); + + // --- RAF loop --- + useEffect(() => { + // This timestamp is a "high-resolution timestamp" provided by + // the browser, which is more accurate for measuring frame intervals + // than Date.now() + const loop = (timestamp: number) => { + console.log("RAF loop, timestamp:", timestamp, lastTime.current); + + if (!lastTime.current) lastTime.current = timestamp; + const delta = (timestamp - lastTime.current) / 1000; + lastTime.current = timestamp; + + // Slow easement towards targetSpeed + currentSpeed.current += + (targetSpeed.current - currentSpeed.current) * SPEED_EASING; + + // flush pending queue into React state + if (pendingQueue.current.length > 0) { + const incoming = pendingQueue.current.splice(0); + setScrollItems((prev) => [...prev, ...incoming]); + } + + // Advance strip, slides it over left by X pixels + translateX.current -= currentSpeed.current * delta; + if (stripRef.current) { + stripRef.current.style.transform = `translateX(${translateX.current}px)`; + } + + // prune items that have fully exited the left edge + if (stripRef.current && containerRef.current) { + const children = Array.from(stripRef.current.children); + const containerLeft = containerRef.current.getBoundingClientRect().left; + let pruneCount = 0; + let removedWidth = 0; + + for (const child of children) { + const rect = child.getBoundingClientRect(); + if (rect.right < containerLeft - 20) { + pruneCount++; + removedWidth += rect.width; + } else { + break; + } + } + if (pruneCount > 0) { + translateX.current += removedWidth; + setScrollItems((prev) => prev.slice(pruneCount)); + } + } + + // Assign the next loop + rafRef.current = requestAnimationFrame(loop); + }; + + rafRef.current = requestAnimationFrame(loop); + return () => cancelAnimationFrame(rafRef.current!); + }, []); + + return ( +
+ {/* Ticker wrapper */} +
+ {/* The ticker band */} +
+ {/* Scrolling strip */} +
+ {scrollItems.map((item) => ( + + ))} + + {/* Trailing spacer so new content doesn't immediately snap in */} + +
- ); +
+
+ ); } diff --git a/jb-ui/src/components/Footer.tsx b/jb-ui/src/components/Footer.tsx index 00c0101..eede06c 100644 --- a/jb-ui/src/components/Footer.tsx +++ b/jb-ui/src/components/Footer.tsx @@ -1,41 +1,51 @@ import { useAppSelector } from "@/hooks"; const Footer = () => { - const bpuid = useAppSelector(state => state.app.bpuid) - const assignment_id = useAppSelector(state => state.app.assignment_id) + const bpuid = useAppSelector((state) => state.app.bpuid); + const assignment_id = useAppSelector((state) => state.app.assignment_id); - return ( -
- + text-xs" + > + -
-

made with ♥ by Max Nanis -

-
+
+

+ made with ♥ by{" "} + + Max Nanis + +

+
- + +
+ ); +}; -
- ) - -} - -export default Footer \ No newline at end of file +export default Footer; diff --git a/jb-ui/src/components/Profiling.tsx b/jb-ui/src/components/Profiling.tsx index 9cefdda..96b06b2 100644 --- a/jb-ui/src/components/Profiling.tsx +++ b/jb-ui/src/components/Profiling.tsx @@ -1,536 +1,562 @@ -import { - UpkQuestionChoice -} from "@/api_fsb"; -import { - ChevronRightIcon -} from "lucide-react"; +import { UpkQuestionChoice } from "@/api_fsb"; +import { ChevronRightIcon } from "lucide-react"; import { KeyboardEvent, MouseEvent, useEffect, useRef, useState } from "react"; import { useAppDispatch, useAppSelector } from "@/hooks"; import { bpid } from "@/lib/utils"; import { - createColumnHelper, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getSortedRowModel, - RowModel, - RowSelectionState, - useReactTable, -} from '@tanstack/react-table'; + createColumnHelper, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + RowModel, + RowSelectionState, + useReactTable, +} from "@tanstack/react-table"; import { clsx } from "clsx"; import { - Pagination, - PaginationContent, - PaginationItem, - PaginationLink + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, } from "@/components/ui/pagination"; import { - addAnswers, - fetchNewQuestions, - goToNextQuestion, - ProfileQuestion, - questionsSelectors, - saveAnswer, - selectActiveQuestion, - selectActiveQuestionValidation, - selectAllQuestions, - setActiveQuestion + addAnswers, + fetchNewQuestions, + goToNextQuestion, + ProfileQuestion, + questionsSelectors, + saveAnswer, + selectActiveQuestion, + selectActiveQuestionValidation, + selectAllQuestions, + setActiveQuestion, } from "@/models/profilingQuestionsSlice"; import { useSelector } from "react-redux"; // import {motion} from "framer-motion" const TextEntry = () => { - const dispatch = useAppDispatch() - - const activeQuestion = useSelector(selectActiveQuestion); - // const selectAnswer = useMemo(() => selectAnswerForQuestion(question), [question]); - // const selectAnswer = useSelector(selectAnswerForQuestion(question)); - const metadata = activeQuestion?._metadata - const validation = activeQuestion?._validation - const bpuid = useAppSelector(state => state.app.bpuid) - - if (!metadata) return null - if (!validation) return null - - const error = validation?.errors.length > 0 - - const handleInputChange = (event: React.KeyboardEvent) => { - if (!activeQuestion.question_id) return; - - const target = event.target as HTMLInputElement; - dispatch(addAnswers({ questionId: activeQuestion.question_id!, answers: [target.value] as string[] })) - }; - - const handleKeyPress = (event: KeyboardEvent) => { - if (!activeQuestion.question_id) return; - if (!bpuid) return; - - if (event.key === 'Enter') { - dispatch(saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid })) - } - }; - - return ( -
- - - { - validation.errors.map(e => { - return ( -

{e.message}

- ) - }) - } -
- ) -} - - -export default function useKeyboardMode( - rowModel: RowModel, -) { - // const [keyboardEnabled, setKeyboardEnabled] = useState( - // localStorage.getItem(keyboardMode) === "enabled", - // ); - const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]); - - const dispatch = useAppDispatch() - const activeQuestion = useSelector(selectActiveQuestion); - const bpuid = useAppSelector(state => state.app.bpuid) - - const keyboardSelectionIdxRef = useRef(0); - - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - - if (!activeQuestion) return; - if (!bpuid) return; - - // if (!keyboardEnabled) { - // return; - // } + const dispatch = useAppDispatch(); + + const activeQuestion = useSelector(selectActiveQuestion); + // const selectAnswer = useMemo(() => selectAnswerForQuestion(question), [question]); + // const selectAnswer = useSelector(selectAnswerForQuestion(question)); + const metadata = activeQuestion?._metadata; + const validation = activeQuestion?._validation; + const bpuid = useAppSelector((state) => state.app.bpuid); + + if (!metadata) return null; + if (!validation) return null; + + const error = validation?.errors.length > 0; + + const handleInputChange = (event: React.KeyboardEvent) => { + if (!activeQuestion.question_id) return; + + const target = event.target as HTMLInputElement; + dispatch( + addAnswers({ + questionId: activeQuestion.question_id!, + answers: [target.value] as string[], + }), + ); + }; + + const handleKeyPress = (event: KeyboardEvent) => { + if (!activeQuestion.question_id) return; + if (!bpuid) return; + + if (event.key === "Enter") { + dispatch( + saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid }), + ); + } + }; + + return ( +
+ - const { key } = event; - const totalRows = rowRefs.current.length; - const prevIdx = keyboardSelectionIdxRef.current; - let nextIdx = prevIdx; + {validation.errors.map((e) => { + return

{e.message}

; + })} +
+ ); +}; - if (key === "ArrowDown") nextIdx = Math.min(prevIdx + 1, totalRows - 1); - if (key === "ArrowUp") nextIdx = Math.max(prevIdx - 1, 0); - if (key === " ") { - const active = document.activeElement as HTMLElement | null; +export default function useKeyboardMode(rowModel: RowModel) { + // const [keyboardEnabled, setKeyboardEnabled] = useState( + // localStorage.getItem(keyboardMode) === "enabled", + // ); + const rowRefs = useRef<(HTMLTableRowElement | null)[]>([]); - if (active?.tabIndex === 0) { - return; - } else { - rowModel.rows[keyboardSelectionIdxRef.current].toggleSelected(); - } - } + const dispatch = useAppDispatch(); + const activeQuestion = useSelector(selectActiveQuestion); + const bpuid = useAppSelector((state) => state.app.bpuid); - if (event.key === 'Enter') { - dispatch(saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid })) - } + const keyboardSelectionIdxRef = useRef(0); + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (!activeQuestion) return; + if (!bpuid) return; - if (nextIdx !== prevIdx) { + // if (!keyboardEnabled) { + // return; + // } - rowRefs.current[prevIdx]?.classList.remove("bg-blue-400"); - rowRefs.current[nextIdx]?.classList.add("bg-blue-400"); + const { key } = event; + const totalRows = rowRefs.current.length; + const prevIdx = keyboardSelectionIdxRef.current; + let nextIdx = prevIdx; - rowRefs.current[nextIdx]?.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - keyboardSelectionIdxRef.current = nextIdx; - } - }; + if (key === "ArrowDown") nextIdx = Math.min(prevIdx + 1, totalRows - 1); + if (key === "ArrowUp") nextIdx = Math.max(prevIdx - 1, 0); + if (key === " ") { + const active = document.activeElement as HTMLElement | null; - // I really don't know, and I don't really care enough to make this - // technically type'd correctly. - Max + if (active?.tabIndex === 0) { + return; + } else { + rowModel.rows[keyboardSelectionIdxRef.current].toggleSelected(); + } + } - // @ts-ignore - window.addEventListener("keydown", handleKeyDown); - // @ts-ignore - return () => window.removeEventListener("keydown", handleKeyDown); + if (event.key === "Enter") { + dispatch( + saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid }), + ); + } - }, [rowModel.rows]); + if (nextIdx !== prevIdx) { + rowRefs.current[prevIdx]?.classList.remove("bg-blue-400"); + rowRefs.current[nextIdx]?.classList.add("bg-blue-400"); - return { - // keyboardEnabled, - // setKeyboardEnabled, - rowRefs, - keyboardSelectionIdxRef, + rowRefs.current[nextIdx]?.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + keyboardSelectionIdxRef.current = nextIdx; + } }; + + // I really don't know, and I don't really care enough to make this + // technically type'd correctly. - Max + + // @ts-ignore + window.addEventListener("keydown", handleKeyDown); + // @ts-ignore + return () => window.removeEventListener("keydown", handleKeyDown); + }, [rowModel.rows]); + + return { + // keyboardEnabled, + // setKeyboardEnabled, + rowRefs, + keyboardSelectionIdxRef, + }; } const MultipleChoice = () => { - const dispatch = useAppDispatch() - const activeQuestion = useSelector(selectActiveQuestion); - - // const error: Boolean = answer?.error_msg.length > 0 - // console.log("Rendering MultipleChoice with answer:", answer, error) - - // TODO! Retrieve store values to use as the initial state for the table - // This is useful for if they tab around before submitting the answer - const [rowSelection, setRowSelection] = useState({}); - const [globalFilter, setGlobalFilter] = useState(''); - - const handleRowSelectionChange = (updaterOrValue: RowSelectionState | ((old: RowSelectionState) => RowSelectionState)) => { - setRowSelection((oldSelection) => { - // Get the new selection state - const newSelection = typeof updaterOrValue === 'function' - ? updaterOrValue(oldSelection) - : updaterOrValue; - - // Find which row was just selected/deselected - const changedRowId = Object.keys(newSelection).find( - key => newSelection[key] !== oldSelection[key] - ); - - if (!changedRowId) return newSelection; - - // Get the actual row data - const row = table.getRow(changedRowId); - const rowData = row.original; // Your row data object - - // Check if "None of the Above" was selected - const normalized = rowData.choice_text.trim().toLowerCase(); - const isNoneOfTheAbove = (normalized === "none of the above") || (rowData.exclusive ?? false); - if (isNoneOfTheAbove && newSelection[changedRowId]) { - // Clear all other selections, keep only this one - return { [changedRowId]: true }; - } - - // Check if a regular option was selected while "None of the Above" exists - const noneOfTheAboveId = Object.keys(newSelection).find(id => { - const r = table.getRow(id); - const rowData = r.original as UpkQuestionChoice; - - const normalized = rowData.choice_text.trim().toLowerCase(); - const isNoneOfTheAbove = (normalized === "none of the above") || (rowData.exclusive ?? false); - - return isNoneOfTheAbove && newSelection[id]; - }); - - if (noneOfTheAboveId && changedRowId !== noneOfTheAboveId) { - // Remove "None of the Above" selection - const { [noneOfTheAboveId]: _, ...rest } = newSelection; - return rest; - } - - return newSelection; - }); - }; + const dispatch = useAppDispatch(); + const activeQuestion = useSelector(selectActiveQuestion); + + // const error: Boolean = answer?.error_msg.length > 0 + // console.log("Rendering MultipleChoice with answer:", answer, error) + + // TODO! Retrieve store values to use as the initial state for the table + // This is useful for if they tab around before submitting the answer + const [rowSelection, setRowSelection] = useState({}); + const [globalFilter, setGlobalFilter] = useState(""); + + const handleRowSelectionChange = ( + updaterOrValue: + | RowSelectionState + | ((old: RowSelectionState) => RowSelectionState), + ) => { + setRowSelection((oldSelection) => { + // Get the new selection state + const newSelection = + typeof updaterOrValue === "function" + ? updaterOrValue(oldSelection) + : updaterOrValue; + + // Find which row was just selected/deselected + const changedRowId = Object.keys(newSelection).find( + (key) => newSelection[key] !== oldSelection[key], + ); + + if (!changedRowId) return newSelection; + + // Get the actual row data + const row = table.getRow(changedRowId); + const rowData = row.original; // Your row data object + + // Check if "None of the Above" was selected + const normalized = rowData.choice_text.trim().toLowerCase(); + const isNoneOfTheAbove = + normalized === "none of the above" || (rowData.exclusive ?? false); + if (isNoneOfTheAbove && newSelection[changedRowId]) { + // Clear all other selections, keep only this one + return { [changedRowId]: true }; + } + + // Check if a regular option was selected while "None of the Above" exists + const noneOfTheAboveId = Object.keys(newSelection).find((id) => { + const r = table.getRow(id); + const rowData = r.original as UpkQuestionChoice; + + const normalized = rowData.choice_text.trim().toLowerCase(); + const isNoneOfTheAbove = + normalized === "none of the above" || (rowData.exclusive ?? false); + + return isNoneOfTheAbove && newSelection[id]; + }); + + if (noneOfTheAboveId && changedRowId !== noneOfTheAboveId) { + // Remove "None of the Above" selection + const { [noneOfTheAboveId]: _, ...rest } = newSelection; + return rest; + } + + return newSelection; + }); + }; + + useEffect(() => { + if (!activeQuestion?.question_id) return; + // Run this anytime a rowSelection changes, to update + // the answer in the store + const selectedChoices = Object.keys(rowSelection); + dispatch( + addAnswers({ + questionId: activeQuestion.question_id, + answers: selectedChoices, + }), + ); + }, [rowSelection]); + + // Define columns + const columnHelper = createColumnHelper(); + const columns = [ + columnHelper.display({ + id: "select", + cell: ({ row }) => { + const canSelected = row.getCanSelect(); - useEffect(() => { - if (!activeQuestion?.question_id) return; - // Run this anytime a rowSelection changes, to update - // the answer in the store - const selectedChoices = Object.keys(rowSelection) - dispatch(addAnswers({ questionId: activeQuestion.question_id, answers: selectedChoices })) - }, [rowSelection]) - - // Define columns - const columnHelper = createColumnHelper() - const columns = [ - columnHelper.display({ - id: 'select', - cell: ({ row }) => { - const canSelected = row.getCanSelect() - - return ( - - ) - }, - size: 20, - meta: { - align: 'center' - } - }), - - columnHelper.accessor("choice_text", { - header: () => 'Choice Text', - cell: (info) => { - const text = info.getValue(); - const selected = info.row.getIsSelected() - const canSelected = info.row.getCanSelect() - - return ( -

+ ); + }, + size: 20, + meta: { + align: "center", + }, + }), + + columnHelper.accessor("choice_text", { + header: () => "Choice Text", + cell: (info) => { + const text = info.getValue(); + const selected = info.row.getIsSelected(); + const canSelected = info.row.getCanSelect(); + + return ( +

- {text} -

- ) - }, - size: 380, - meta: { - align: 'left' - } - }), - ] - - const table = useReactTable({ - 'data': activeQuestion!.choices ?? [], - columns, - getRowId: (row) => row.choice_id, - state: { - rowSelection, - globalFilter, - }, - // enableRowSelection: true, - enableRowSelection: (row) => { - // Allow deselection always, but limit new selections - const currentSelectionCount = Object.keys(rowSelection).length; - const isRowSelected = rowSelection[row.id]; - - const max_selection = activeQuestion?.selector === "SA" ? 1 : activeQuestion?.choices?.length ?? 10 - - // Allow if: row is already selected OR we haven't hit the limit - return isRowSelected || currentSelectionCount < max_selection; - }, - onRowSelectionChange: handleRowSelectionChange, - onGlobalFilterChange: setGlobalFilter, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - }); - - const selectedCount = table.getSelectedRowModel().rows.length; - - //const { rowRefs, setKeyboardEnabled, keyboardSelectionIdxRef } = useKeyboardMode(table.getRowModel()); - const { rowRefs } = useKeyboardMode(table.getRowModel()); - - return ( - <> - - {/* Controls */} -
-
- - {/* Search */} -
- setGlobalFilter(e.target.value)} - className="w-full px-4 py-1 + ${selected ? "font-bold" : "font-medium"} + ${canSelected ? "text-blue-950" : "text-blue-600"} + `} + > + {text} +

+ ); + }, + size: 380, + meta: { + align: "left", + }, + }), + ]; + + const table = useReactTable({ + data: activeQuestion!.choices ?? [], + columns, + getRowId: (row) => row.choice_id, + state: { + rowSelection, + globalFilter, + }, + // enableRowSelection: true, + enableRowSelection: (row) => { + // Allow deselection always, but limit new selections + const currentSelectionCount = Object.keys(rowSelection).length; + const isRowSelected = rowSelection[row.id]; + + const max_selection = + activeQuestion?.selector === "SA" + ? 1 + : (activeQuestion?.choices?.length ?? 10); + + // Allow if: row is already selected OR we haven't hit the limit + return isRowSelected || currentSelectionCount < max_selection; + }, + onRowSelectionChange: handleRowSelectionChange, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + }); + + const selectedCount = table.getSelectedRowModel().rows.length; + + //const { rowRefs, setKeyboardEnabled, keyboardSelectionIdxRef } = useKeyboardMode(table.getRowModel()); + const { rowRefs } = useKeyboardMode(table.getRowModel()); + + return ( + <> + {/* Controls */} +
+
+ {/* Search */} +
+ setGlobalFilter(e.target.value)} + className="w-full px-4 py-1 border border-blue-900 bg-blue-400 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500" - /> -
+ /> +
- {/* Selected Count */} - {selectedCount > 0 && ( -
setRowSelection({})}> - {selectedCount} selected - -
- )} - -
+ onClick={() => setRowSelection({})} + > + {selectedCount} selected +
+ )} +
+
- {/* Table */} -
- - - {table.getRowModel().rows.map((row, idx) => ( - { - rowRefs.current[idx] = el; - }} - onClick={() => row.toggleSelected()} - className={`cursor-pointer transition-colors w-full - ${row.getIsSelected() ? 'bg-blue-600 ' : ' ' + " + > +
+ + {table.getRowModel().rows.map((row, idx) => ( + { + rowRefs.current[idx] = el; + }} + onClick={() => row.toggleSelected()} + className={`cursor-pointer transition-colors w-full + ${ + row.getIsSelected() ? "bg-blue-600 " : " " }`} - > - - {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(cell.column.columnDef.cell, cell.getContext())} -
-
- - {/* Empty State */} - {table.getRowModel().rows.length === 0 && ( -
- No items found matching your search -
- )} + > + {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( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ); + })} + + ))} + + + + + {/* Empty State */} + {table.getRowModel().rows.length === 0 && ( +
+ No items found matching your search +
+ )} - {/* Footer */} -
- - Showing {table.getRowModel().rows.length} of {activeQuestion?.choices?.length} choices - - {selectedCount > 0 && ( - - {selectedCount} choice{selectedCount !== 1 ? 's' : ''} selected - - )} -
- - - ) - -} + " + > + + Showing {table.getRowModel().rows.length} of{" "} + {activeQuestion?.choices?.length} choices + + {selectedCount > 0 && ( + + {selectedCount} choice{selectedCount !== 1 ? "s" : ""} selected + + )} + + + ); +}; const ProfilingQuestionTitle = () => { - const activeQuestion = useSelector(selectActiveQuestion); - const question_active_tasks = activeQuestion?.importance?.task_count ?? 0 + const activeQuestion = useSelector(selectActiveQuestion); + const question_active_tasks = activeQuestion?.importance?.task_count ?? 0; - if (!activeQuestion) return null; - - let text = activeQuestion?.question_text + if (!activeQuestion) return null; - if (activeQuestion.explanation_template && activeQuestion._validation?.isComplete && activeQuestion._metadata.answers.length == 1) { - const template = activeQuestion.explanation_template; - const pre_code = activeQuestion._metadata.answers[0]; + let text = activeQuestion?.question_text; - const choice = activeQuestion.choices?.find(c => c.choice_id === pre_code) - const choice_text = choice ? choice.choice_text : "your answer" + if ( + activeQuestion.explanation_template && + activeQuestion._validation?.isComplete && + activeQuestion._metadata.answers.length == 1 + ) { + const template = activeQuestion.explanation_template; + const pre_code = activeQuestion._metadata.answers[0]; - text = template.replace("{answer}", choice_text) - } + const choice = activeQuestion.choices?.find( + (c) => c.choice_id === pre_code, + ); + const choice_text = choice ? choice.choice_text : "your answer"; + text = template.replace("{answer}", choice_text); + } - return ( -
- -

+

- {text} -

- -
-

- {question_active_tasks.toLocaleString()} -

-
- Surveys use - this question -
-
-
- ) -} + " + > + {text} + + +
+

+ {question_active_tasks.toLocaleString()} +

+
Surveys use this question
+
+ + ); +}; const ProfilingQuestionSubmitButton = () => { - const dispatch = useAppDispatch() - - const activeQuestion = useSelector(selectActiveQuestion); - const activeQuestionValidation = useSelector(selectActiveQuestionValidation) - const bpuid = useAppSelector(state => state.app.bpuid) - - const handleOnClick = () => { - if (!activeQuestion) return - if (!activeQuestionValidation?.isComplete) return - if (!bpuid) return; - - dispatch(saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid })) - } - - return ( - - ) -} - + disabled={!activeQuestionValidation?.isComplete} + onClick={handleOnClick} + > + Answer Question + + ); +}; export const ProfileQuestionFull = () => { - const activeQuestion = useSelector(selectActiveQuestion); - - const renderContent = () => { - if (!activeQuestion) return null; - switch (activeQuestion.question_type) { - case 'TE': - return - case 'MC': - return - default: - return - } - }; + const activeQuestion = useSelector(selectActiveQuestion); + + const renderContent = () => { + if (!activeQuestion) return null; + switch (activeQuestion.question_type) { + case "TE": + return ; + case "MC": + return ; + default: + return ; + } + }; - if (!activeQuestion) return null + if (!activeQuestion) return null; - return ( - <> - {/* {answer && answer.processing && ( + return ( + <> + {/* {answer && answer.processing && ( { /> )} */} - {renderContent()} - - ) -} + {renderContent()} + + ); +}; const PaginationIcon: React.FC<{ - question: ProfileQuestion, idx: number, + question: ProfileQuestion; + idx: number; }> = ({ question, idx }) => { - const dispatch = useAppDispatch() + const dispatch = useAppDispatch(); - // const answers = useAppSelector(state => state.answers) - const isActive = question._metadata?.isActive ?? false - const completed = question._validation?.isComplete ?? false + // const answers = useAppSelector(state => state.answers) + const isActive = question._metadata?.isActive ?? false; + const completed = question._validation?.isComplete ?? false; - const handleSetQuestion = (evt: MouseEvent) => { - if (completed) { - evt.preventDefault() - } else { - dispatch(setActiveQuestion(question.question_id!)) - } + const handleSetQuestion = (evt: MouseEvent) => { + if (completed) { + evt.preventDefault(); + } else { + dispatch(setActiveQuestion(question.question_id!)); } - - return ( - - - {idx + 1} - - - ) -} + }; + + return ( + + + {idx + 1} + + + ); +}; const ProfilingQuestionPagination = () => { - const dispatch = useAppDispatch() - - const questions = useSelector(selectAllQuestions) - const activeQuestion = useSelector(selectActiveQuestion) - - // All the variables needed for a sliding window - const q_idx = questions.findIndex(q => q.question_id === activeQuestion!.question_id) - - const handleNextClick = () => { - dispatch(goToNextQuestion()); - }; - - const questionsWindow = ( - items: ProfileQuestion[], currentIndex: number, windowSize: number = 5 - ): ProfileQuestion[] => { - const half: number = Math.floor(windowSize / 2) - const total: number = items.length - let start: number = currentIndex - half - let end: number = currentIndex + half + 1 - - if (start < 0) { - end += Math.abs(start) - start = 0 - } - - // Adjust if window goes past the end - if (end > total) { - const overflow: number = end - total - start = Math.max(0, start - overflow) - end = total - } + const dispatch = useAppDispatch(); + + const questions = useSelector(selectAllQuestions); + const activeQuestion = useSelector(selectActiveQuestion); + + // All the variables needed for a sliding window + const q_idx = questions.findIndex( + (q) => q.question_id === activeQuestion!.question_id, + ); + + const handleNextClick = () => { + dispatch(goToNextQuestion()); + }; + + const questionsWindow = ( + items: ProfileQuestion[], + currentIndex: number, + windowSize: number = 5, + ): ProfileQuestion[] => { + const half: number = Math.floor(windowSize / 2); + const total: number = items.length; + let start: number = currentIndex - half; + let end: number = currentIndex + half + 1; + + if (start < 0) { + end += Math.abs(start); + start = 0; + } - return items.slice(start, end) + // Adjust if window goes past the end + if (end > total) { + const overflow: number = end - total; + start = Math.max(0, start - overflow); + end = total; } - return ( - - - { - questionsWindow(questions, q_idx).map(q => { - return qq.question_id === q.question_id)} - /> - }) - } - - - - Next - - - - - - - ) -} + onClick={handleNextClick} + href="#" + > + Next + + + + + + ); +}; const ProfilingHeader = () => { - const questions = useSelector(questionsSelectors.selectAll); + const questions = useSelector(questionsSelectors.selectAll); - return ( -

- {questions.length} Profiling Questions -

- ) -} + return ( +

+ {questions.length} Profiling Questions +

+ ); +}; const ProfilingModalBody = () => { - const activeQuestion = useSelector(selectActiveQuestion); - - if (!activeQuestion) { - return ( -
-

No Profiling Questions Available

-

Please check back later.

-
- ) - } + const activeQuestion = useSelector(selectActiveQuestion); + if (!activeQuestion) { return ( -
- {/* */} - -
- -
- - {/* Scrollable Content */} -
- -
- - {/* Sticky Footer */} -
-
- -
-
- -
-
- +
+

No Profiling Questions Available

+

Please check back later.

+
+ ); + } + + return ( +
+ {/* */} + +
+ +
+ + {/* Scrollable Content */} +
+ +
+ + {/* Sticky Footer */} +
+
+
- ) -} +
+ +
+
+
+ ); +}; const Profiling = () => { - /* This is the main UI for any User Profiling (before entering a task). It's + /* This is the main UI for any User Profiling (before entering a task). It's primary window and position should be dedictated by it's parent so this can be positioned as a window or a sidebar as needed. */ - const [showProfiling, setShowProfiling] = useState(false); + const [showProfiling, setShowProfiling] = useState(false); - const dispatch = useAppDispatch() - const bpuid = useAppSelector(state => state.app.bpuid) + const dispatch = useAppDispatch(); + const bpuid = useAppSelector((state) => state.app.bpuid); - useEffect(() => { - if (!bpuid) return; - dispatch(fetchNewQuestions(bpuid)); - }, [bpid, bpuid]) + useEffect(() => { + if (!bpuid) return; + dispatch(fetchNewQuestions(bpuid)); + }, [bpid, bpuid]); - if (!bpuid) return null; + if (!bpuid) return null; - return ( -
- -
+
setShowProfiling(!showProfiling)} - > - - -

- (Click to {showProfiling ? 'hide' : 'show'}) -

-
- - {showProfiling && ( - - )} - -
- ) -} - -export { - Profiling + onClick={() => setShowProfiling(!showProfiling)} + > + + +

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

+
+ + {showProfiling && } +
+ ); }; + +export { Profiling }; diff --git a/jb-ui/src/components/Wallet.tsx b/jb-ui/src/components/Wallet.tsx index e4fc694..933dc9a 100644 --- a/jb-ui/src/components/Wallet.tsx +++ b/jb-ui/src/components/Wallet.tsx @@ -1,464 +1,491 @@ import { - UserLedgerTransactionsResponse, - UserLedgerTransactionsResponseTransactionsInner, - WalletApi + UserLedgerTransactionsResponse, + UserLedgerTransactionsResponseTransactionsInner, + WalletApi, } from "@/api_fsb"; import { useAppDispatch, useAppSelector } from "@/hooks"; -import { bpid, formatCentsToUSD, } from "@/lib/utils"; +import { bpid, formatCentsToUSD } from "@/lib/utils"; import { - setTxPagination, setTxTotalItems, setTxTotalPages, - setUserLedgerSummary, - setUserLedgerTxs + setTxPagination, + setTxTotalItems, + setTxTotalPages, + setUserLedgerSummary, + setUserLedgerTxs, } from "@/models/appSlice"; import { - RowData, createColumnHelper, flexRender, getCoreRowModel, - useReactTable + RowData, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, } from "@tanstack/react-table"; import moment from "moment"; import { useEffect, useState } from "react"; -declare module '@tanstack/react-table' { - interface ColumnMeta { - align?: 'left' | 'center' | 'right'; - } +declare module "@tanstack/react-table" { + interface ColumnMeta { + align?: "left" | "center" | "right"; + } } // Function to calculate background color based on balance const getBackgroundColor = (balance: number | undefined): string => { - if (balance === undefined || balance === null) return 'bg-blue-600'; - - if (balance > 0) { - return 'bg-blue-600'; - } else if (balance <= -100) { - return 'bg-red-600'; - } else { - // Interpolate between blue and red for values between 0 and -100 - // progress goes from 0 (at balance = 0) to 1 (at balance = -100) - const progress = Math.abs(balance) / 100; - - // Blue RGB: (37, 99, 235) - Tailwind blue-600 - // Red RGB: (220, 38, 38) - Tailwind red-600 - const r = Math.round(37 + (220 - 37) * progress); - const g = Math.round(99 + (38 - 99) * progress); - const b = Math.round(235 + (38 - 235) * progress); - - return `rgb(${r}, ${g}, ${b})`; - } + if (balance === undefined || balance === null) return "bg-blue-600"; + + if (balance > 0) { + return "bg-blue-600"; + } else if (balance <= -100) { + return "bg-red-600"; + } else { + // Interpolate between blue and red for values between 0 and -100 + // progress goes from 0 (at balance = 0) to 1 (at balance = -100) + const progress = Math.abs(balance) / 100; + + // Blue RGB: (37, 99, 235) - Tailwind blue-600 + // Red RGB: (220, 38, 38) - Tailwind red-600 + const r = Math.round(37 + (220 - 37) * progress); + const g = Math.round(99 + (38 - 99) * progress); + const b = Math.round(235 + (38 - 235) * progress); + + return `rgb(${r}, ${g}, ${b})`; + } }; - const WalletHeader = () => { - const amount = useAppSelector(state => state.app.userWalletBalance)?.amount ?? 0 - - const bgStyle = typeof getBackgroundColor(amount) === 'string' && - getBackgroundColor(amount).startsWith('bg-') - ? {} - : { backgroundColor: getBackgroundColor(amount) }; - - const bgClass = typeof getBackgroundColor(amount) === 'string' && - getBackgroundColor(amount).startsWith('bg-') - ? getBackgroundColor(amount) - : ''; - - return ( -
-

- User Wallet: - - {amount >= 0 ? ( - - {`${formatCentsToUSD(Math.abs(amount))}`} - - ) : ( - - {`(${formatCentsToUSD(Math.abs(amount))})`} - - )} - -

-
- ) + const amount = + useAppSelector((state) => state.app.userWalletBalance)?.amount ?? 0; + + const bgStyle = + typeof getBackgroundColor(amount) === "string" && + getBackgroundColor(amount).startsWith("bg-") + ? {} + : { backgroundColor: getBackgroundColor(amount) }; + + const bgClass = + typeof getBackgroundColor(amount) === "string" && + getBackgroundColor(amount).startsWith("bg-") + ? getBackgroundColor(amount) + : ""; + + return ( +
+

+ User Wallet: + + {amount >= 0 ? ( + + {`${formatCentsToUSD(Math.abs(amount))}`} + + ) : ( + + {`(${formatCentsToUSD(Math.abs(amount))})`} + + )} +

+
+ ); }; const WalletSummary = () => { - const summary = useAppSelector(state => state.app.userLedgerSummary) - - const survey_completes = summary?.bp_payment?.entry_count ?? 0 - const survey_completes_f = (survey_completes ?? 0).toLocaleString('en-US') - - const survey_adjustments = summary?.bp_adjustment?.entry_count ?? 0 - const survey_adjustments_f = (survey_adjustments ?? 0).toLocaleString('en-US') - - const min_payout: number = summary?.bp_payment?.min_amount ?? 0 - const max_payout: number = summary?.bp_payment?.max_amount ?? 0 - const adj_total: number = summary?.bp_adjustment?.total_amount ?? 0 - const user_payments_total: number = (summary?.user_payout_request?.total_amount ?? 0) * -1 - - return ( -
-
Completes:
-
{survey_completes_f} surveys
- -
Avg Payouts:
-
{formatCentsToUSD(min_payout)} – {formatCentsToUSD(max_payout)}
- -
Survey Adjustments:
-
{survey_adjustments_f}
- -
Adjustment Amount:
-
- {adj_total >= 0 ? ( - - {`${formatCentsToUSD(adj_total)}`} - - ) : ( - - {`(${formatCentsToUSD(Math.abs(adj_total))})`} - - )} -
- -
User Payments:
-
- {user_payments_total >= 0 ? ( - - {`${formatCentsToUSD(user_payments_total)}`} - - ) : ( - - {`(${formatCentsToUSD(Math.abs(user_payments_total))})`} - - )} - -
-
- ) + const summary = useAppSelector((state) => state.app.userLedgerSummary); + + const survey_completes = summary?.bp_payment?.entry_count ?? 0; + const survey_completes_f = (survey_completes ?? 0).toLocaleString("en-US"); + + const survey_adjustments = summary?.bp_adjustment?.entry_count ?? 0; + const survey_adjustments_f = (survey_adjustments ?? 0).toLocaleString( + "en-US", + ); + + const min_payout: number = summary?.bp_payment?.min_amount ?? 0; + const max_payout: number = summary?.bp_payment?.max_amount ?? 0; + const adj_total: number = summary?.bp_adjustment?.total_amount ?? 0; + const user_payments_total: number = + (summary?.user_payout_request?.total_amount ?? 0) * -1; + + return ( +
+
Completes:
+
{survey_completes_f} surveys
+ +
Avg Payouts:
+
+ {formatCentsToUSD(min_payout)} – {formatCentsToUSD(max_payout)} +
+ +
Survey Adjustments:
+
{survey_adjustments_f}
+ +
Adjustment Amount:
+
+ {adj_total >= 0 ? ( + + {`${formatCentsToUSD(adj_total)}`} + + ) : ( + + {`(${formatCentsToUSD(Math.abs(adj_total))})`} + + )} +
+ +
User Payments:
+
+ {user_payments_total >= 0 ? ( + + {`${formatCentsToUSD(user_payments_total)}`} + + ) : ( + + {`(${formatCentsToUSD(Math.abs(user_payments_total))})`} + + )} +
+
+ ); }; const WalletTransactionsHeader = () => { - const tx_cnt = useAppSelector(state => state.app.txTotalItems) - const txt_cnt_f = (tx_cnt ?? 0).toLocaleString('en-US') - - const [showTx, setShowTx] = useState(false); - - return ( -
-
setShowTx(!showTx)}> - -

+
setShowTx(!showTx)} + > +

setShowTx(!showTx)} - > - Total Transactions: {txt_cnt_f} -

- -

setShowTx(!showTx)} - > - (Click to {showTx ? 'hide' : 'show'}) -

- -
- - {showTx && ( - <> - - - )} + onClick={() => setShowTx(!showTx)} + > + Total Transactions: {txt_cnt_f} +

-
- ) +

setShowTx(!showTx)} + > + (Click to {showTx ? "hide" : "show"}) +

+
+ + {showTx && ( + <> + + + )} + + ); }; - const WalletTransactions = () => { - const dispatch = useAppDispatch() - const bpuid = useAppSelector(state => state.app.bpuid) - const userLedgerTxs = useAppSelector(state => state.app.userLedgerTxs); - - const txPagination = useAppSelector(state => state.app.txPagination); - const txTotalPages = useAppSelector(state => state.app.txTotalPages); - - useEffect(() => { - if (!bpuid) return; - - new WalletApi().getUserTransactionHistoryProductIdTransactionHistoryGet( - bpid, // productId - bpuid, // bpuid - undefined, // createdAfter - undefined, // createdBefore - "-created", // orderBy - txPagination.pageIndex + 1, // page - txPagination.pageSize // pageSize - ).then(res => { - const response = res.data as UserLedgerTransactionsResponse; - console.log("Wallet: fetched user ledger", response); - dispatch(setUserLedgerSummary(response.summary)); - - dispatch(setUserLedgerTxs(response.transactions ?? [])) - dispatch(setTxTotalItems(response.total!)) - dispatch(setTxTotalPages(response.pages!)) - }); - - }, [bpuid, txPagination.pageIndex, txPagination.pageSize]); - - const columnHelper = createColumnHelper() - const columns = [ - columnHelper.accessor('created', { - header: () => 'Date', - cell: (info) => moment(info.getValue()).format('M/D/YY H:mm'), - size: 110, - meta: { - align: 'left' - } - }), - columnHelper.accessor('description', { - header: () => 'Type', - cell: props => { - const desc = props.getValue(); - switch (desc) { - case 'Task Complete': - return 'Survey 🎉'; - case 'Task Adjustment': - return 'Reject ⚠️'; - case 'Compensation Bonus': - return 'Bonus 🎁'; - case 'HIT Bonus': - return 'Bonus'; - case 'HIT Reward': - return 'Assignment'; - default: - return desc; - } - }, - size: 80, - meta: { - align: 'left' - } - }), - columnHelper.accessor('amount', { - header: () => 'Amount', - cell: (props) => { - const val = props.renderValue() as number; - const isPositive = val >= 0; - const emoji = isPositive ? "⬆\uFE0E" : "⬇\uFE0E"; - const colorClass = isPositive ? 'font-variant-emoji-text text-emerald-300' : 'font-variant-emoji-text text-rose-300'; + const dispatch = useAppDispatch(); + const bpuid = useAppSelector((state) => state.app.bpuid); + const userLedgerTxs = useAppSelector((state) => state.app.userLedgerTxs); + + const txPagination = useAppSelector((state) => state.app.txPagination); + const txTotalPages = useAppSelector((state) => state.app.txTotalPages); + + useEffect(() => { + if (!bpuid) return; + + new WalletApi() + .getUserTransactionHistoryProductIdTransactionHistoryGet( + bpid, // productId + bpuid, // bpuid + undefined, // createdAfter + undefined, // createdBefore + "-created", // orderBy + txPagination.pageIndex + 1, // page + txPagination.pageSize, // pageSize + ) + .then((res) => { + const response = res.data as UserLedgerTransactionsResponse; + console.log("Wallet: fetched user ledger", response); + dispatch(setUserLedgerSummary(response.summary)); + + dispatch(setUserLedgerTxs(response.transactions ?? [])); + dispatch(setTxTotalItems(response.total!)); + dispatch(setTxTotalPages(response.pages!)); + }); + }, [bpuid, txPagination.pageIndex, txPagination.pageSize]); + + const columnHelper = + createColumnHelper(); + const columns = [ + columnHelper.accessor("created", { + header: () => "Date", + cell: (info) => moment(info.getValue()).format("M/D/YY H:mm"), + size: 110, + meta: { + align: "left", + }, + }), + columnHelper.accessor("description", { + header: () => "Type", + cell: (props) => { + const desc = props.getValue(); + switch (desc) { + case "Task Complete": + return "Survey 🎉"; + case "Task Adjustment": + return "Reject ⚠️"; + case "Compensation Bonus": + return "Bonus 🎁"; + case "HIT Bonus": + return "Bonus"; + case "HIT Reward": + return "Assignment"; + default: + return desc; + } + }, + size: 80, + meta: { + align: "left", + }, + }), + columnHelper.accessor("amount", { + header: () => "Amount", + cell: (props) => { + const val = props.renderValue() as number; + const isPositive = val >= 0; + const emoji = isPositive ? "⬆\uFE0E" : "⬇\uFE0E"; + const colorClass = isPositive + ? "font-variant-emoji-text text-emerald-300" + : "font-variant-emoji-text text-rose-300"; + + return ( + + {`${emoji} ${formatCentsToUSD(Math.abs(val))}`} + + ); + }, + size: 70, + meta: { + align: "center", + }, + }), + columnHelper.accessor("balance_after", { + header: () => "Balance", + cell: (props) => { + const val = props.renderValue() as number; + const isPositive = val >= 0; + const colorClass = isPositive ? "text-emerald-300" : "text-rose-300"; + + return ( + <> + {isPositive ? ( + + {`${formatCentsToUSD(Math.abs(val))}`} + + ) : ( + + {`(${formatCentsToUSD(Math.abs(val))})`} + + )} + + ); + }, + size: 70, + meta: { + align: "center", + }, + }), + ]; + + const table = useReactTable({ + data: userLedgerTxs, + columns: columns, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + pageCount: txTotalPages, + onPaginationChange: (updater) => { + dispatch( + setTxPagination( + typeof updater === "function" ? updater(txPagination) : updater, + ), + ); + }, + state: { + pagination: txPagination, + }, + }); + + return ( +
+ + + {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 ( - - {`${emoji} ${formatCentsToUSD(Math.abs(val))}`} - - ) - }, - size: 70, - meta: { - align: 'center' - } - }), - columnHelper.accessor('balance_after', { - header: () => 'Balance', - cell: (props) => { - const val = props.renderValue() as number; - const isPositive = val >= 0; - const colorClass = isPositive ? 'text-emerald-300' : 'text-rose-300'; + + ); + })} + + ))} + + + {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 ( - <> - {isPositive ? ( - - {`${formatCentsToUSD(Math.abs(val))}`} - - ) : ( - - {`(${formatCentsToUSD(Math.abs(val))})`} - - )} - - ) - }, - size: 70, - meta: { - align: 'center' - } - }), - ] - - const table = useReactTable({ - 'data': userLedgerTxs, - 'columns': columns, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, - pageCount: txTotalPages, - onPaginationChange: (updater) => { - dispatch(setTxPagination( - typeof updater === 'function' ? updater(txPagination) : updater - )); - }, - state: { - pagination: txPagination, - }, - }); - - return ( -
-
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} +
- - {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 */} -
-
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ {/* Page info */} +
+ Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} +
+ + {/* Navigation buttons */} +
+ - + -
- - {/* Page size selector */} - 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 => ( - - ))} - -
-
-
- ) + > + {[10, 20, 30, 40].map((pageSize) => ( + + ))} + + + + + + + + ); }; - const Wallet = () => { - const dispatch = useAppDispatch() - const bpuid = useAppSelector(state => state.app.bpuid) - const pagination = useAppSelector(state => state.app.txPagination); - - useEffect(() => { - if (!bpuid) return; - - new WalletApi().getUserTransactionHistoryProductIdTransactionHistoryGet( - bpid, // productId - bpuid, // bpuid - undefined, // createdAfter - undefined, // createdBefore - "-created", // orderBy - pagination.pageIndex + 1, // page - pagination.pageSize // pageSize - ).then(res => { - const response = res.data as UserLedgerTransactionsResponse; - dispatch(setUserLedgerSummary(response.summary)); - dispatch(setUserLedgerTxs(response.transactions ?? [])) - - dispatch(setTxPagination({ - pageIndex: (response.page ?? 1) - 1, - pageSize: response.size ?? 10, - })) - dispatch(setTxTotalItems(response.total ?? 0)) - dispatch(setTxTotalPages(response.pages ?? 9)) - }); - - }, [bpuid]) - - if (!bpuid) return null; - - return ( -
{ + const response = res.data as UserLedgerTransactionsResponse; + dispatch(setUserLedgerSummary(response.summary)); + dispatch(setUserLedgerTxs(response.transactions ?? [])); + + dispatch( + setTxPagination({ + pageIndex: (response.page ?? 1) - 1, + pageSize: response.size ?? 10, + }), + ); + dispatch(setTxTotalItems(response.total ?? 0)); + dispatch(setTxTotalPages(response.pages ?? 9)); + }); + }, [bpuid]); + + if (!bpuid) return null; + + return ( +
- - - -
- ) + " + > + + + +
+ ); }; - -export default Wallet; \ No newline at end of file +export default Wallet; diff --git a/jb-ui/src/components/ui/button.tsx b/jb-ui/src/components/ui/button.tsx index b5ea4ab..d2176b4 100644 --- a/jb-ui/src/components/ui/button.tsx +++ b/jb-ui/src/components/ui/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { Slot } from "radix-ui" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { Slot } from "radix-ui"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", @@ -35,8 +35,8 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } -) + }, +); function Button({ className, @@ -46,9 +46,9 @@ function Button({ ...props }: React.ComponentProps<"button"> & VariantProps & { - asChild?: boolean + asChild?: boolean; }) { - const Comp = asChild ? Slot.Root : "button" + const Comp = asChild ? Slot.Root : "button"; return ( - ) + ); } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/jb-ui/src/components/ui/checkbox.tsx b/jb-ui/src/components/ui/checkbox.tsx index 0e2a6cd..f24ad45 100644 --- a/jb-ui/src/components/ui/checkbox.tsx +++ b/jb-ui/src/components/ui/checkbox.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import * as CheckboxPrimitive from "@radix-ui/react-checkbox" -import { CheckIcon } from "lucide-react" +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { CheckIcon } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Checkbox({ className, @@ -13,7 +13,7 @@ function Checkbox({ data-slot="checkbox" className={cn( "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50", - className + className, )} {...props} > @@ -24,7 +24,7 @@ function Checkbox({ - ) + ); } -export { Checkbox } +export { Checkbox }; diff --git a/jb-ui/src/components/ui/confetti.tsx b/jb-ui/src/components/ui/confetti.tsx index e0d353a..57d3273 100644 --- a/jb-ui/src/components/ui/confetti.tsx +++ b/jb-ui/src/components/ui/confetti.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from "react" +import type { ReactNode } from "react"; import React, { createContext, forwardRef, @@ -7,30 +7,30 @@ import React, { useImperativeHandle, useMemo, useRef, -} from "react" +} from "react"; import type { GlobalOptions as ConfettiGlobalOptions, CreateTypes as ConfettiInstance, Options as ConfettiOptions, -} from "canvas-confetti" -import confetti from "canvas-confetti" +} from "canvas-confetti"; +import confetti from "canvas-confetti"; -import { Button } from "@/components/ui/button" +import { Button } from "@/components/ui/button"; type Api = { - fire: (options?: ConfettiOptions) => void -} + fire: (options?: ConfettiOptions) => void; +}; type Props = React.ComponentPropsWithRef<"canvas"> & { - options?: ConfettiOptions - globalOptions?: ConfettiGlobalOptions - manualstart?: boolean - children?: ReactNode -} + options?: ConfettiOptions; + globalOptions?: ConfettiGlobalOptions; + manualstart?: boolean; + children?: ReactNode; +}; -export type ConfettiRef = Api | null +export type ConfettiRef = Api | null; -const ConfettiContext = createContext({} as Api) +const ConfettiContext = createContext({} as Api); // Define component first const ConfettiComponent = forwardRef((props, ref) => { @@ -40,76 +40,76 @@ const ConfettiComponent = forwardRef((props, ref) => { manualstart = false, children, ...rest - } = props - const instanceRef = useRef(null) + } = props; + const instanceRef = useRef(null); const canvasRef = useCallback( (node: HTMLCanvasElement) => { if (node !== null) { - if (instanceRef.current) return + if (instanceRef.current) return; instanceRef.current = confetti.create(node, { ...globalOptions, resize: true, - }) + }); } else { if (instanceRef.current) { - instanceRef.current.reset() - instanceRef.current = null + instanceRef.current.reset(); + instanceRef.current = null; } } }, - [globalOptions] - ) + [globalOptions], + ); const fire = useCallback( async (opts = {}) => { try { - await instanceRef.current?.({ ...options, ...opts }) + await instanceRef.current?.({ ...options, ...opts }); } catch (error) { - console.error("Confetti error:", error) + console.error("Confetti error:", error); } }, - [options] - ) + [options], + ); const api = useMemo( () => ({ fire, }), - [fire] - ) + [fire], + ); - useImperativeHandle(ref, () => api, [api]) + useImperativeHandle(ref, () => api, [api]); useEffect(() => { if (!manualstart) { - ;(async () => { + (async () => { try { - await fire() + await fire(); } catch (error) { - console.error("Confetti effect error:", error) + console.error("Confetti effect error:", error); } - })() + })(); } - }, [manualstart, fire]) + }, [manualstart, fire]); return ( {children} - ) -}) + ); +}); // Set display name immediately -ConfettiComponent.displayName = "Confetti" +ConfettiComponent.displayName = "Confetti"; // Export as Confetti -export const Confetti = ConfettiComponent +export const Confetti = ConfettiComponent; interface ConfettiButtonProps extends React.ComponentProps<"button"> { options?: ConfettiOptions & - ConfettiGlobalOptions & { canvas?: HTMLCanvasElement } + ConfettiGlobalOptions & { canvas?: HTMLCanvasElement }; } const ConfettiButtonComponent = ({ @@ -119,28 +119,28 @@ const ConfettiButtonComponent = ({ }: ConfettiButtonProps) => { const handleClick = async (event: React.MouseEvent) => { try { - const rect = event.currentTarget.getBoundingClientRect() - const x = rect.left + rect.width / 2 - const y = rect.top + rect.height / 2 + const rect = event.currentTarget.getBoundingClientRect(); + const x = rect.left + rect.width / 2; + const y = rect.top + rect.height / 2; await confetti({ ...options, origin: { x: x / window.innerWidth, y: y / window.innerHeight, }, - }) + }); } catch (error) { - console.error("Confetti button error:", error) + console.error("Confetti button error:", error); } - } + }; return ( - ) -} + ); +}; -ConfettiButtonComponent.displayName = "ConfettiButton" +ConfettiButtonComponent.displayName = "ConfettiButton"; -export const ConfettiButton = ConfettiButtonComponent +export const ConfettiButton = ConfettiButtonComponent; diff --git a/jb-ui/src/components/ui/dot-pattern.tsx b/jb-ui/src/components/ui/dot-pattern.tsx index 9522b7f..a217a9b 100644 --- a/jb-ui/src/components/ui/dot-pattern.tsx +++ b/jb-ui/src/components/ui/dot-pattern.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useId, useRef, useState } from "react" -import { motion } from "motion/react" +import React, { useEffect, useId, useRef, useState } from "react"; +import { motion } from "motion/react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; /** * DotPattern Component Props @@ -17,16 +17,16 @@ import { cn } from "@/lib/utils" * @param {boolean} [glow=false] - Whether dots should have a glowing animation effect */ interface DotPatternProps extends React.SVGProps { - width?: number - height?: number - x?: number - y?: number - cx?: number - cy?: number - cr?: number - className?: string - glow?: boolean - [key: string]: unknown + width?: number; + height?: number; + x?: number; + y?: number; + cx?: number; + cy?: number; + cr?: number; + className?: string; + glow?: boolean; + [key: string]: unknown; } /** @@ -71,22 +71,22 @@ export function DotPattern({ glow = false, ...props }: DotPatternProps) { - const id = useId() - const containerRef = useRef(null) - const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) + const id = useId(); + const containerRef = useRef(null); + const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); useEffect(() => { const updateDimensions = () => { if (containerRef.current) { - const { width, height } = containerRef.current.getBoundingClientRect() - setDimensions({ width, height }) + const { width, height } = containerRef.current.getBoundingClientRect(); + setDimensions({ width, height }); } - } + }; - updateDimensions() - window.addEventListener("resize", updateDimensions) - return () => window.removeEventListener("resize", updateDimensions) - }, []) + updateDimensions(); + window.addEventListener("resize", updateDimensions); + return () => window.removeEventListener("resize", updateDimensions); + }, []); const dots = Array.from( { @@ -95,16 +95,16 @@ export function DotPattern({ Math.ceil(dimensions.height / height), }, (_, i) => { - const col = i % Math.ceil(dimensions.width / width) - const row = Math.floor(i / Math.ceil(dimensions.width / width)) + const col = i % Math.ceil(dimensions.width / width); + const row = Math.floor(i / Math.ceil(dimensions.width / width)); return { x: col * width + cx, y: row * height + cy, delay: Math.random() * 5, duration: Math.random() * 3 + 2, - } - } - ) + }; + }, + ); return ( - ) + ); } diff --git a/jb-ui/src/components/ui/field.tsx b/jb-ui/src/components/ui/field.tsx index db0dc12..77546e5 100644 --- a/jb-ui/src/components/ui/field.tsx +++ b/jb-ui/src/components/ui/field.tsx @@ -1,9 +1,9 @@ -import { useMemo } from "react" -import { cva, type VariantProps } from "class-variance-authority" +import { useMemo } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" -import { Label } from "@/components/ui/label" -import { Separator } from "@/components/ui/separator" +import { cn } from "@/lib/utils"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { return ( @@ -12,11 +12,11 @@ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { className={cn( "flex flex-col gap-6", "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", - className + className, )} {...props} /> - ) + ); } function FieldLegend({ @@ -32,11 +32,11 @@ function FieldLegend({ "mb-3 font-medium", "data-[variant=legend]:text-base", "data-[variant=label]:text-sm", - className + className, )} {...props} /> - ) + ); } function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { @@ -45,11 +45,11 @@ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { data-slot="field-group" className={cn( "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4", - className + className, )} {...props} /> - ) + ); } const fieldVariants = cva( @@ -73,8 +73,8 @@ const fieldVariants = cva( defaultVariants: { orientation: "vertical", }, - } -) + }, +); function Field({ className, @@ -89,7 +89,7 @@ function Field({ className={cn(fieldVariants({ orientation }), className)} {...props} /> - ) + ); } function FieldContent({ className, ...props }: React.ComponentProps<"div">) { @@ -98,11 +98,11 @@ function FieldContent({ className, ...props }: React.ComponentProps<"div">) { data-slot="field-content" className={cn( "group/field-content flex flex-1 flex-col gap-1.5 leading-snug", - className + className, )} {...props} /> - ) + ); } function FieldLabel({ @@ -116,11 +116,11 @@ function FieldLabel({ "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50", "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4", "has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10", - className + className, )} {...props} /> - ) + ); } function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { @@ -129,11 +129,11 @@ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { data-slot="field-label" className={cn( "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50", - className + className, )} {...props} /> - ) + ); } function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { @@ -144,11 +144,11 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { "text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance", "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5", "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", - className + className, )} {...props} /> - ) + ); } function FieldSeparator({ @@ -156,7 +156,7 @@ function FieldSeparator({ className, ...props }: React.ComponentProps<"div"> & { - children?: React.ReactNode + children?: React.ReactNode; }) { return (
@@ -178,7 +178,7 @@ function FieldSeparator({ )}
- ) + ); } function FieldError({ @@ -187,37 +187,37 @@ function FieldError({ errors, ...props }: React.ComponentProps<"div"> & { - errors?: Array<{ message?: string } | undefined> + errors?: Array<{ message?: string } | undefined>; }) { const content = useMemo(() => { if (children) { - return children + return children; } if (!errors?.length) { - return null + return null; } const uniqueErrors = [ ...new Map(errors.map((error) => [error?.message, error])).values(), - ] + ]; if (uniqueErrors?.length == 1) { - return uniqueErrors[0]?.message + return uniqueErrors[0]?.message; } return (
    {uniqueErrors.map( (error, index) => - error?.message &&
  • {error.message}
  • + error?.message &&
  • {error.message}
  • , )}
- ) - }, [children, errors]) + ); + }, [children, errors]); if (!content) { - return null + return null; } return ( @@ -229,7 +229,7 @@ function FieldError({ > {content} - ) + ); } export { @@ -243,4 +243,4 @@ export { FieldSet, FieldContent, FieldTitle, -} +}; diff --git a/jb-ui/src/components/ui/label.tsx b/jb-ui/src/components/ui/label.tsx index ef7133a..4ef28a9 100644 --- a/jb-ui/src/components/ui/label.tsx +++ b/jb-ui/src/components/ui/label.tsx @@ -1,7 +1,7 @@ -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Label({ className, @@ -12,11 +12,11 @@ function Label({ data-slot="label" className={cn( "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50", - className + className, )} {...props} /> - ) + ); } -export { Label } +export { Label }; diff --git a/jb-ui/src/components/ui/magic-card.tsx b/jb-ui/src/components/ui/magic-card.tsx index e6503d8..2b92e38 100644 --- a/jb-ui/src/components/ui/magic-card.tsx +++ b/jb-ui/src/components/ui/magic-card.tsx @@ -1,16 +1,16 @@ -import React, { useCallback, useEffect } from "react" -import { motion, useMotionTemplate, useMotionValue } from "motion/react" +import React, { useCallback, useEffect } from "react"; +import { motion, useMotionTemplate, useMotionValue } from "motion/react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; interface MagicCardProps { - children?: React.ReactNode - className?: string - gradientSize?: number - gradientColor?: string - gradientOpacity?: number - gradientFrom?: string - gradientTo?: string + children?: React.ReactNode; + className?: string; + gradientSize?: number; + gradientColor?: string; + gradientOpacity?: number; + gradientFrom?: string; + gradientTo?: string; } export function MagicCard({ @@ -22,49 +22,49 @@ export function MagicCard({ gradientFrom = "#9E7AFF", gradientTo = "#FE8BBB", }: MagicCardProps) { - const mouseX = useMotionValue(-gradientSize) - const mouseY = useMotionValue(-gradientSize) + const mouseX = useMotionValue(-gradientSize); + const mouseY = useMotionValue(-gradientSize); const reset = useCallback(() => { - mouseX.set(-gradientSize) - mouseY.set(-gradientSize) - }, [gradientSize, mouseX, mouseY]) + mouseX.set(-gradientSize); + mouseY.set(-gradientSize); + }, [gradientSize, mouseX, mouseY]); const handlePointerMove = useCallback( (e: React.PointerEvent) => { - const rect = e.currentTarget.getBoundingClientRect() - mouseX.set(e.clientX - rect.left) - mouseY.set(e.clientY - rect.top) + const rect = e.currentTarget.getBoundingClientRect(); + mouseX.set(e.clientX - rect.left); + mouseY.set(e.clientY - rect.top); }, - [mouseX, mouseY] - ) + [mouseX, mouseY], + ); useEffect(() => { - reset() - }, [reset]) + reset(); + }, [reset]); useEffect(() => { const handleGlobalPointerOut = (e: PointerEvent) => { if (!e.relatedTarget) { - reset() + reset(); } - } + }; const handleVisibility = () => { if (document.visibilityState !== "visible") { - reset() + reset(); } - } + }; - window.addEventListener("pointerout", handleGlobalPointerOut) - window.addEventListener("blur", reset) - document.addEventListener("visibilitychange", handleVisibility) + window.addEventListener("pointerout", handleGlobalPointerOut); + window.addEventListener("blur", reset); + document.addEventListener("visibilitychange", handleVisibility); return () => { - window.removeEventListener("pointerout", handleGlobalPointerOut) - window.removeEventListener("blur", reset) - document.removeEventListener("visibilitychange", handleVisibility) - } - }, [reset]) + window.removeEventListener("pointerout", handleGlobalPointerOut); + window.removeEventListener("blur", reset); + document.removeEventListener("visibilitychange", handleVisibility); + }; + }, [reset]); return (
{children}
- ) + ); } diff --git a/jb-ui/src/components/ui/pagination.tsx b/jb-ui/src/components/ui/pagination.tsx index 1dcfb0c..990f907 100644 --- a/jb-ui/src/components/ui/pagination.tsx +++ b/jb-ui/src/components/ui/pagination.tsx @@ -1,12 +1,12 @@ -import * as React from "react" +import * as React from "react"; import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon, -} from "lucide-react" +} from "lucide-react"; -import { cn } from "@/lib/utils" -import { buttonVariants, type Button } from "@/components/ui/button" +import { cn } from "@/lib/utils"; +import { buttonVariants, type Button } from "@/components/ui/button"; function Pagination({ className, ...props }: React.ComponentProps<"nav">) { return ( @@ -17,7 +17,7 @@ function Pagination({ className, ...props }: React.ComponentProps<"nav">) { className={cn("mx-auto flex w-full justify-center", className)} {...props} /> - ) + ); } function PaginationContent({ @@ -30,17 +30,17 @@ function PaginationContent({ className={cn("flex flex-row items-center gap-1", className)} {...props} /> - ) + ); } function PaginationItem({ ...props }: React.ComponentProps<"li">) { - return
  • + return
  • ; } type PaginationLinkProps = { - isActive?: boolean + isActive?: boolean; } & Pick, "size"> & - React.ComponentProps<"a"> + React.ComponentProps<"a">; function PaginationLink({ className, @@ -58,11 +58,11 @@ function PaginationLink({ variant: isActive ? "outline" : "ghost", size, }), - className + className, )} {...props} /> - ) + ); } function PaginationPrevious({ @@ -79,7 +79,7 @@ function PaginationPrevious({ Previous - ) + ); } function PaginationNext({ @@ -96,7 +96,7 @@ function PaginationNext({ Next - ) + ); } function PaginationEllipsis({ @@ -113,7 +113,7 @@ function PaginationEllipsis({ More pages - ) + ); } export { @@ -124,4 +124,4 @@ export { PaginationPrevious, PaginationNext, PaginationEllipsis, -} +}; diff --git a/jb-ui/src/components/ui/separator.tsx b/jb-ui/src/components/ui/separator.tsx index 275381c..72c18e3 100644 --- a/jb-ui/src/components/ui/separator.tsx +++ b/jb-ui/src/components/ui/separator.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import * as React from "react" -import * as SeparatorPrimitive from "@radix-ui/react-separator" +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Separator({ className, @@ -18,11 +18,11 @@ function Separator({ orientation={orientation} className={cn( "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", - className + className, )} {...props} /> - ) + ); } -export { Separator } +export { Separator }; diff --git a/jb-ui/src/components/ui/skeleton.tsx b/jb-ui/src/components/ui/skeleton.tsx index 32ea0ef..0168998 100644 --- a/jb-ui/src/components/ui/skeleton.tsx +++ b/jb-ui/src/components/ui/skeleton.tsx @@ -1,4 +1,4 @@ -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Skeleton({ className, ...props }: React.ComponentProps<"div">) { return ( @@ -7,7 +7,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) { className={cn("bg-accent animate-pulse rounded-md", className)} {...props} /> - ) + ); } -export { Skeleton } +export { Skeleton }; -- cgit v1.2.3 From cb5e40f9b5fb0ae4bb9a769dd7a9c83c2af18a65 Mon Sep 17 00:00:00 2001 From: Max Nanis Date: Fri, 11 Sep 2026 07:49:51 -0700 Subject: Transfer page. Jenkins CDN. Layout url check --- jb-ui/Jenkinsfile | 6 +- jb-ui/src/JBApp.tsx | 18 ++-- jb-ui/src/components/EventMarquee.tsx | 2 +- jb-ui/src/pages/LinkAmtAccount.tsx | 84 ----------------- jb-ui/src/pages/Transfer.tsx | 171 ++++++++++++++++++++++++++++------ jb-ui/vite.config.ts | 102 ++++++++++---------- 6 files changed, 207 insertions(+), 176 deletions(-) delete mode 100644 jb-ui/src/pages/LinkAmtAccount.tsx (limited to 'jb-ui/src/components') diff --git a/jb-ui/Jenkinsfile b/jb-ui/Jenkinsfile index 27a0ab9..faf63de 100644 --- a/jb-ui/Jenkinsfile +++ b/jb-ui/Jenkinsfile @@ -82,7 +82,7 @@ pipeline { expression { env.BRANCH_NAME == 'master' } } steps { - sh "/usr/bin/rsync -avz ${WORKSPACE}/jb-ui/${env.BUILD_DIR}/ thl-cdn:/root/amt-jb-cdn/" + sh "/usr/bin/rsync -avz ${WORKSPACE}/jb-ui/${env.BUILD_DIR}/ cdn-jb:/var/www/amt-jb-cdn/" } } @@ -101,7 +101,7 @@ pipeline { sh "cp ${env.JS_FILENAME}.js ${versioned_js}.js" sh "cp ${env.CSS_FILENAME}.js ${versioned_css}.js" - sh "/usr/bin/rsync -avz ${WORKSPACE}/jb-ui/${env.BUILD_DIR}/ thl-cdn:/root/amt-jb-cdn/" + sh "/usr/bin/rsync -avz ${WORKSPACE}/jb-ui/${env.BUILD_DIR}/ cdn-jb:/var/www/amt-jb-cdn/" } } } @@ -126,7 +126,7 @@ pipeline { sh "cp ${env.JS_FILENAME}.js ${versioned}.js" sh "cp ${env.CSS_FILENAME}.js ${versioned}.js" - sh "/usr/bin/rsync -avz ${WORKSPACE}/jb-ui/${env.BUILD_DIR}/ thl-cdn:/root/amt-jb-cdn/" + sh "/usr/bin/rsync -avz ${WORKSPACE}/jb-ui/${env.BUILD_DIR}/ cdn-jb:/var/www/amt-jb-cdn/" } } } diff --git a/jb-ui/src/JBApp.tsx b/jb-ui/src/JBApp.tsx index 472b976..7c99315 100644 --- a/jb-ui/src/JBApp.tsx +++ b/jb-ui/src/JBApp.tsx @@ -27,11 +27,15 @@ import Home from "@/pages/Home"; import MagicLink from "@/pages/MagicLink"; import Preview from "@/pages/Preview"; import Result from "@/pages/Result"; +import clsx from "clsx"; + import Work from "@/pages/Work"; import Transfer from "@/pages/Transfer"; import { BrowserRouter, Outlet, + useLocation, + useMatch, Route, Routes, useSearchParams, @@ -118,6 +122,9 @@ function QueryParamProcessor() { } const Layout = () => { + const location = useLocation(); + const isSpecialSubview = useMatch("/work/*"); + return (
    @@ -134,13 +141,10 @@ const Layout = () => {
    diff --git a/jb-ui/src/components/EventMarquee.tsx b/jb-ui/src/components/EventMarquee.tsx index 6aab3f8..46508ff 100644 --- a/jb-ui/src/components/EventMarquee.tsx +++ b/jb-ui/src/components/EventMarquee.tsx @@ -148,7 +148,7 @@ export default function NewsTicker() { // the browser, which is more accurate for measuring frame intervals // than Date.now() const loop = (timestamp: number) => { - console.log("RAF loop, timestamp:", timestamp, lastTime.current); + // console.log("RAF loop, timestamp:", timestamp, lastTime.current); if (!lastTime.current) lastTime.current = timestamp; const delta = (timestamp - lastTime.current) / 1000; diff --git a/jb-ui/src/pages/LinkAmtAccount.tsx b/jb-ui/src/pages/LinkAmtAccount.tsx deleted file mode 100644 index a72626f..0000000 --- a/jb-ui/src/pages/LinkAmtAccount.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { type FormEvent, useState } from "react"; - -import { Button } from "@/components/ui/button"; - -type RequestState = "idle" | "working" | "sent" | "failed" | "missing-worker"; - -const LinkAmtAccount = function () { - const [email, setEmail] = useState(""); - const [state, setState] = useState("idle"); - const [mockLink, setMockLink] = useState(null); - const amtWorkerId = new URLSearchParams(window.location.search).get( - "workerId", - ); - - const submit = async (event: FormEvent) => { - event.preventDefault(); - if (!amtWorkerId) { - setState("missing-worker"); - return; - } - - setState("working"); - setMockLink(null); - try { - const response = await fetch("/auth/link-amt/request", { - method: "POST", - credentials: "include", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - email: email, - amt_worker_id: amtWorkerId, - }), - }); - if (!response.ok) throw new Error("Account-link request failed"); - const body = (await response.json()) as { magic_link?: string }; - setMockLink(body.magic_link ?? null); - setState("sent"); - } catch { - setState("failed"); - } - }; - - return ( -
    -
    -

    Link your MTurk account

    -

    Enter the email address you want to use for James Billings.

    -
    - setEmail(event.target.value)} - placeholder="you@example.com" - /> - -
    - {state === "sent" && ( -

    Check your email to finish linking your account.

    - )} - {state === "failed" && ( -

    Unable to send the link. Please try again.

    - )} - {state === "missing-worker" && ( -

    This page is missing your MTurk worker ID.

    - )} - {mockLink && ( -

    - Development link:{" "} - - continue - -

    - )} -
    -
    - ); -}; - -export default LinkAmtAccount; diff --git a/jb-ui/src/pages/Transfer.tsx b/jb-ui/src/pages/Transfer.tsx index 6e087d6..bef40d4 100644 --- a/jb-ui/src/pages/Transfer.tsx +++ b/jb-ui/src/pages/Transfer.tsx @@ -1,50 +1,167 @@ -import { clsx } from "clsx"; +import { type FormEvent, useState } from "react"; +import { useAppDispatch, useAppSelector } from "@/hooks"; +import { Button } from "@/components/ui/button"; +import max_lulu from "./max-and-lulu.jpg"; -import { useSelector } from "react-redux"; +// const handleInputChange = ( +// event: React.KeyboardEvent, +// ) => {}; -const Transfer = function () { - let error = []; +// const handleKeyPress = (event: KeyboardEvent) => {}; +// const handleSubmit = (e) => { +// e.preventDefault(); +// console.log("Form submitted successfully!"); +// }; + +const TransferComplete = function () { + return ( +
    +

    + Please check your email. +

    +

    + If you don't recieve anything, please check your spam folder, or try + again shortly. +

    +

    + We will be assisting account transfers throughout the rest of the month. + If you have any further questions or concerns, please email us directly + with your desired account email and the AMT ID to associate with your + worker history. +

    + Max and Lulu +
    + ); +}; + +type RequestState = "idle" | "working" | "sent" | "failed"; - const handleInputChange = ( - event: React.KeyboardEvent, - ) => {}; +const TransferSubmit = function ({ onSubmitSuccess }) { + const [email, setEmail] = useState(""); + const [state, setState] = useState("idle"); + const bpuid = useAppSelector((state) => state.app.bpuid); - const handleKeyPress = (event: KeyboardEvent) => {}; + const submit = async (event: FormEvent) => { + event.preventDefault(); + setState("working"); - const handleSubmit = (e) => { - e.preventDefault(); - console.log("Form submitted successfully!"); + try { + const response = await fetch("/auth/link-amt/request", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: email, + amt_worker_id: bpuid, + }), + }); + + if (!response.ok) throw new Error("Account-link request failed"); + + const body = (await response.json()) as { magic_link?: string }; + console.log(body.magic_link ?? null); + onSubmitSuccess(); + } catch { + setState("failed"); + } }; return (

    Transfer Required

    -

    - James Billings is finished 😭 Just Kidding!!! -

    + James Billings is no longer available on Amazon Mechanical Turk. Our + dedicated platform will enable you to do the same work as always, paid + directly to you. +
    + Enter your email below to transfer your account history. +
    + This will allow you to keep your full account history and wallet + balance. + + +
    + setEmail(event.target.value)} + placeholder="you@example.com" + /> + +
    - + {state === "sent" && ( +

    + Check your email to finish linking your account. +

    + )} + + {state === "failed" &&

    Unable to send the link. Please try again.

    }
    ); }; +const Transfer = function () { + const assignment_id = useAppSelector((state) => state.app.assignment_id); + const bpuid = useAppSelector((state) => state.app.bpuid); + const [submitted, setSubmitted] = useState(false); + + if (!bpuid) { + return ( +

    + No Worker ID found. Please access this page through the Amazon + Mechanical Turk platform. +

    + ); + } + + if (!assignment_id) { + return ( +

    + No Assignment ID found. Please access this page through the Amazon + Mechanical Turk platform. +

    + ); + } + + return ( + <> + {submitted ? ( + + ) : ( + setSubmitted(true)} /> + )} + + ); +}; + export default Transfer; diff --git a/jb-ui/vite.config.ts b/jb-ui/vite.config.ts index 6b0e530..2336e4a 100644 --- a/jb-ui/vite.config.ts +++ b/jb-ui/vite.config.ts @@ -1,62 +1,56 @@ -import react from "@vitejs/plugin-react" -import {defineConfig} from "vite" -import path from 'path'; -import tailwindcss from '@tailwindcss/vite' - +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; +import path from "path"; +import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ - plugins: [ - react(), - tailwindcss(), - - ], + plugins: [react(), tailwindcss()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), }, - - server: { - // host: "0.0.0.0", - // port: 8001, - - // This forwards requests to the FastAPI development server - // that must also be running - proxy: { - '^/(status|statuses|report|survey)(/|$)': { - target: 'http://localhost:8000', - changeOrigin: true, - } - } + }, + + server: { + // host: "0.0.0.0", + // port: 8001, + + // This forwards requests to the FastAPI development server + // that must also be running + proxy: { + "^/(auth|status|statuses|report|survey)(/|$)": { + target: "http://localhost:8000", + changeOrigin: true, + }, }, + }, - define: { - 'process.env.NODE_ENV': JSON.stringify('production') - }, + define: { + "process.env.NODE_ENV": JSON.stringify("production"), + }, - build: { - cssCodeSplit: false, - lib: { - name: "jb-ui", - entry: 'src/main.tsx', + build: { + cssCodeSplit: false, + lib: { + name: "jb-ui", + entry: "src/main.tsx", + }, + rollupOptions: { + output: { + entryFileNames: "james-billings.js", + assetFileNames: (assetInfo) => { + if (assetInfo.names[0].endsWith(".css")) { + return "james-has-style.css"; + } + + return "assets/[name].[hash][extname]"; }, - rollupOptions: { - output: { - entryFileNames: 'james-billings.js', - assetFileNames: (assetInfo) => { - - if (assetInfo.names[0].endsWith('.css')) { - return 'james-has-style.css'; - } - - return 'assets/[name].[hash][extname]'; - }, - globals: { - react: 'React', - 'react-dom': 'ReactDOM' - } - } - } - } -}) \ No newline at end of file + globals: { + react: "React", + "react-dom": "ReactDOM", + }, + }, + }, + }, +}); -- cgit v1.2.3 From f82ac61b6a044467ab37c0645c3fe99b40bedfc5 Mon Sep 17 00:00:00 2001 From: Max Nanis Date: Fri, 11 Sep 2026 23:06:59 -0700 Subject: replace references to bpuid with logged in user --- jb-ui/src/JBApp.tsx | 14 +++----- jb-ui/src/components/Footer.tsx | 7 ++-- jb-ui/src/components/Profiling.tsx | 38 ++++++++++++-------- jb-ui/src/components/Wallet.tsx | 19 +++++----- jb-ui/src/models/authAPI.ts | 3 +- jb-ui/src/pages/Preview.tsx | 3 +- jb-ui/src/pages/Transfer.tsx | 9 ++--- jb-ui/src/pages/Work.tsx | 71 +++++++++----------------------------- jb-ui/src/types/authApi.ts | 6 ++++ 9 files changed, 73 insertions(+), 97 deletions(-) create mode 100644 jb-ui/src/types/authApi.ts (limited to 'jb-ui/src/components') diff --git a/jb-ui/src/JBApp.tsx b/jb-ui/src/JBApp.tsx index fd0eeba..a69cc85 100644 --- a/jb-ui/src/JBApp.tsx +++ b/jb-ui/src/JBApp.tsx @@ -16,6 +16,7 @@ import { import Footer from "@/components/Footer"; import { useAppDispatch } from "@/hooks"; import { bpid, routeBasename, tagManager } from "@/lib/utils"; +import { useGetMeQuery } from "@/models/authAPI"; import { setAssignmentID, setProductUserID, @@ -72,24 +73,19 @@ function isPingMessage(msg: Message): msg is PingMessage { function QueryParamProcessor() { const dispatch = useAppDispatch(); const [searchParams] = useSearchParams(); + const { data: user, isLoading, isError } = useGetMeQuery(); useEffect(() => { - const worker_id = searchParams.get("workerId"); const assignment_id = searchParams.get("assignmentId"); const tsid = searchParams.get("tsid"); - const turkSubmitTo = searchParams.get("turkSubmitTo"); - if (turkSubmitTo) { - dispatch(setTurkSubmitTo(turkSubmitTo)); - } - - if (worker_id) { - dispatch(setProductUserID(worker_id)); + if (user?.product_user_id) { + dispatch(setProductUserID(user.product_user_id)); new WalletApi() .getUserWalletBalanceProductIdWalletGet( bpid, // productId - worker_id, // bpuid + user.product_user_id, ) .then((res) => { const response = res.data as UserWalletBalanceResponse; diff --git a/jb-ui/src/components/Footer.tsx b/jb-ui/src/components/Footer.tsx index eede06c..ec458f2 100644 --- a/jb-ui/src/components/Footer.tsx +++ b/jb-ui/src/components/Footer.tsx @@ -1,8 +1,7 @@ -import { useAppSelector } from "@/hooks"; +import { useGetMeQuery } from "@/models/authAPI"; const Footer = () => { - const bpuid = useAppSelector((state) => state.app.bpuid); - const assignment_id = useAppSelector((state) => state.app.assignment_id); + const { data: user, isLoading, isError } = useGetMeQuery(); return (
    {

    help:{" "} support@jamesbillings67.com diff --git a/jb-ui/src/components/Profiling.tsx b/jb-ui/src/components/Profiling.tsx index 96b06b2..23b8457 100644 --- a/jb-ui/src/components/Profiling.tsx +++ b/jb-ui/src/components/Profiling.tsx @@ -1,4 +1,5 @@ import { UpkQuestionChoice } from "@/api_fsb"; +import { useGetMeQuery } from "@/models/authAPI"; import { ChevronRightIcon } from "lucide-react"; import { KeyboardEvent, MouseEvent, useEffect, useRef, useState } from "react"; @@ -46,7 +47,7 @@ const TextEntry = () => { // const selectAnswer = useSelector(selectAnswerForQuestion(question)); const metadata = activeQuestion?._metadata; const validation = activeQuestion?._validation; - const bpuid = useAppSelector((state) => state.app.bpuid); + const { data: user, isLoading, isError } = useGetMeQuery(); if (!metadata) return null; if (!validation) return null; @@ -67,11 +68,14 @@ const TextEntry = () => { const handleKeyPress = (event: KeyboardEvent) => { if (!activeQuestion.question_id) return; - if (!bpuid) return; + if (!user?.product_user_id) return; if (event.key === "Enter") { dispatch( - saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid }), + saveAnswer({ + questionId: activeQuestion.question_id!, + bpuid: user.product_user_id, + }), ); } }; @@ -109,14 +113,14 @@ export default function useKeyboardMode(rowModel: RowModel) { const dispatch = useAppDispatch(); const activeQuestion = useSelector(selectActiveQuestion); - const bpuid = useAppSelector((state) => state.app.bpuid); + const { data: user, isLoading, isError } = useGetMeQuery(); const keyboardSelectionIdxRef = useRef(0); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (!activeQuestion) return; - if (!bpuid) return; + if (!user?.product_user_id) return; // if (!keyboardEnabled) { // return; @@ -141,7 +145,10 @@ export default function useKeyboardMode(rowModel: RowModel) { if (event.key === "Enter") { dispatch( - saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid }), + saveAnswer({ + questionId: activeQuestion.question_id!, + bpuid: user.product_user_id, + }), ); } @@ -507,15 +514,18 @@ const ProfilingQuestionSubmitButton = () => { const activeQuestion = useSelector(selectActiveQuestion); const activeQuestionValidation = useSelector(selectActiveQuestionValidation); - const bpuid = useAppSelector((state) => state.app.bpuid); + const { data: user, isLoading, isError } = useGetMeQuery(); const handleOnClick = () => { if (!activeQuestion) return; if (!activeQuestionValidation?.isComplete) return; - if (!bpuid) return; + if (!user?.product_user_id) return; dispatch( - saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid }), + saveAnswer({ + questionId: activeQuestion.question_id!, + bpuid: user.product_user_id, + }), ); }; @@ -749,14 +759,14 @@ const Profiling = () => { const [showProfiling, setShowProfiling] = useState(false); const dispatch = useAppDispatch(); - const bpuid = useAppSelector((state) => state.app.bpuid); + const { data: user, isLoading, isError } = useGetMeQuery(); useEffect(() => { - if (!bpuid) return; - dispatch(fetchNewQuestions(bpuid)); - }, [bpid, bpuid]); + if (!user?.product_user_id) return; + dispatch(fetchNewQuestions(user.product_user_id)); + }, [bpid, user?.product_user_id]); - if (!bpuid) return null; + if (!user?.product_user_id) return null; return (

    { const WalletTransactions = () => { const dispatch = useAppDispatch(); - const bpuid = useAppSelector((state) => state.app.bpuid); + const { data: user, isLoading, isError } = useGetMeQuery(); const userLedgerTxs = useAppSelector((state) => state.app.userLedgerTxs); const txPagination = useAppSelector((state) => state.app.txPagination); const txTotalPages = useAppSelector((state) => state.app.txTotalPages); useEffect(() => { - if (!bpuid) return; + if (!user?.product_user_id) return; new WalletApi() .getUserTransactionHistoryProductIdTransactionHistoryGet( bpid, // productId - bpuid, // bpuid + user.product_user_id, undefined, // createdAfter undefined, // createdBefore "-created", // orderBy @@ -215,7 +216,7 @@ const WalletTransactions = () => { dispatch(setTxTotalItems(response.total!)); dispatch(setTxTotalPages(response.pages!)); }); - }, [bpuid, txPagination.pageIndex, txPagination.pageSize]); + }, [user?.product_user_id, txPagination.pageIndex, txPagination.pageSize]); const columnHelper = createColumnHelper(); @@ -438,16 +439,16 @@ const WalletTransactions = () => { const Wallet = () => { const dispatch = useAppDispatch(); - const bpuid = useAppSelector((state) => state.app.bpuid); const pagination = useAppSelector((state) => state.app.txPagination); + const { data: user, isLoading, isError } = useGetMeQuery(); useEffect(() => { - if (!bpuid) return; + if (!user?.product_user_id) return; new WalletApi() .getUserTransactionHistoryProductIdTransactionHistoryGet( bpid, // productId - bpuid, // bpuid + user.product_user_id, undefined, // createdAfter undefined, // createdBefore "-created", // orderBy @@ -468,9 +469,9 @@ const Wallet = () => { dispatch(setTxTotalItems(response.total ?? 0)); dispatch(setTxTotalPages(response.pages ?? 9)); }); - }, [bpuid]); + }, [user?.product_user_id]); - if (!bpuid) return null; + if (!user?.product_user_id) return null; return (
    ({ - getMe: builder.query({ + getMe: builder.query({ query: () => "/session", providesTags: ["User"], }), diff --git a/jb-ui/src/pages/Preview.tsx b/jb-ui/src/pages/Preview.tsx index 6e50b0d..1926975 100644 --- a/jb-ui/src/pages/Preview.tsx +++ b/jb-ui/src/pages/Preview.tsx @@ -95,7 +95,6 @@ export const Leaderboards = () => { const [payouts, setPayouts] = useState(null); const [completes, setCompletes] = useState(null); - // const bpuid = useAppSelector(state => state.app.bpuid) const bpuid = undefined; useEffect(() => { @@ -105,7 +104,7 @@ export const Leaderboards = () => { LeaderboardCode.CompleteCount, frequency, "us", - bpuid, // bpuid + bpuid, undefined, // within_time 10, ) diff --git a/jb-ui/src/pages/Transfer.tsx b/jb-ui/src/pages/Transfer.tsx index bef40d4..4192eee 100644 --- a/jb-ui/src/pages/Transfer.tsx +++ b/jb-ui/src/pages/Transfer.tsx @@ -1,5 +1,6 @@ import { type FormEvent, useState } from "react"; import { useAppDispatch, useAppSelector } from "@/hooks"; +import { useGetMeQuery } from "@/models/authAPI"; import { Button } from "@/components/ui/button"; import max_lulu from "./max-and-lulu.jpg"; @@ -43,7 +44,7 @@ type RequestState = "idle" | "working" | "sent" | "failed"; const TransferSubmit = function ({ onSubmitSuccess }) { const [email, setEmail] = useState(""); const [state, setState] = useState("idle"); - const bpuid = useAppSelector((state) => state.app.bpuid); + const { data: user, isLoading, isError } = useGetMeQuery(); const submit = async (event: FormEvent) => { event.preventDefault(); @@ -56,7 +57,7 @@ const TransferSubmit = function ({ onSubmitSuccess }) { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email, - amt_worker_id: bpuid, + amt_worker_id: user?.product_user_id, }), }); @@ -132,10 +133,10 @@ const TransferSubmit = function ({ onSubmitSuccess }) { const Transfer = function () { const assignment_id = useAppSelector((state) => state.app.assignment_id); - const bpuid = useAppSelector((state) => state.app.bpuid); + const { data: user, isLoading, isError } = useGetMeQuery(); const [submitted, setSubmitted] = useState(false); - if (!bpuid) { + if (!user?.product_user_id) { return (

    No Worker ID found. Please access this page through the Amazon diff --git a/jb-ui/src/pages/Work.tsx b/jb-ui/src/pages/Work.tsx index e33328e..84cb946 100644 --- a/jb-ui/src/pages/Work.tsx +++ b/jb-ui/src/pages/Work.tsx @@ -51,8 +51,7 @@ import Markdown from "react-markdown"; const Report = ({ onClose }: { onClose: () => void }) => { const [showComplete, setShowComplete] = useState(false); const [selectedValues, setSelectedValues] = useState([]); - - const bpuid = useAppSelector((state) => state.app.bpuid); + const { data: user, isLoading, isError } = useGetMeQuery(); const ts = useAppSelector((state) => state.app.taskStatus); const handleCheckboxChange = (value: ReportValue, checked: boolean) => { @@ -62,12 +61,12 @@ const Report = ({ onClose }: { onClose: () => void }) => { }; const clicked_submit = () => { - if (!bpuid) return; + if (!user?.product_user_id) return; // ReportValue.NUMBER_0 console.log("selectedValues:", selectedValues); const rt: ReportTask = { - bpuid: bpuid, + bpuid: user.product_user_id, reasons: selectedValues, notes: "", }; @@ -122,9 +121,10 @@ const Report = ({ onClose }: { onClose: () => void }) => { return

    Please finish a Task to report its status.

    ; } - if (showComplete) { - return ; - } + // TODO! What do we do here, redirect back to / with a thank you + wallet refresh? + // if (showComplete) { + // return ; + // } return (
    @@ -189,43 +189,6 @@ const Report = ({ onClose }: { onClose: () => void }) => { ); }; -const SubimtAMT = () => { - const bpuid = useAppSelector((state) => state.app.bpuid); - const assignment_id = useAppSelector((state) => state.app.assignment_id); - const ts = useAppSelector((state) => state.app.taskStatus); - const turkSubmitTo = useAppSelector((state) => state.app.turkSubmitTo); - - const formRef = useRef(null); - - useEffect(() => { - if (!ts) return; - if (!bpuid) return; - if (!assignment_id) return; - if (!turkSubmitTo) return; - - if (formRef.current) { - formRef.current.submit(); - } - }, [ts?.tsid, bpuid, assignment_id, turkSubmitTo]); - - return ( - <> -
    - - - - - -
    - - ); -}; - const SurveyMonitor = () => { const dispatch = useAppDispatch(); @@ -235,7 +198,7 @@ const SurveyMonitor = () => { const [showComplete, setShowComplete] = useState(false); const [showReport, setShowReport] = useState(false); - const bpuid = useAppSelector((state) => state.app.bpuid); + const { data: user, isLoading, isError } = useGetMeQuery(); const entered = useAppSelector((state) => state.app.currentBucketEntered); const isLow = useAppSelector((state) => isLowBalance(state)); @@ -263,7 +226,7 @@ const SurveyMonitor = () => { // }, [ts]); useEffect(() => { - if (!bpuid) return; + if (!user?.product_user_id) return; if (!entered) return; if (!isPolling) return; @@ -271,7 +234,7 @@ const SurveyMonitor = () => { new StatusApi() .listTaskStatusesProductIdStatusGet( bpid, // productId - bpuid, // Worker ID + user.product_user_id, // Worker ID entered, // startedAfter moment.utc().unix(), // startedBefore ) @@ -418,8 +381,9 @@ const SurveyMonitor = () => { Report Survey
    - - {showComplete && } + {/* TODO! What do we do here, redirect back to / with wallet */} + {/* TODO! refresh? The confetti is on a different page. */} + {/* {showComplete && } */} {showReport && setShowReport(false)} />} ) : ( @@ -731,7 +695,7 @@ const BucketDetails = () => { const SurveyEnter = () => { const dispatch = useAppDispatch(); const loi = useAppSelector((state) => state.app.loi); - const bpuid = useAppSelector((state) => state.app.bpuid); + const { data: user, isLoading, isError } = useGetMeQuery(); const available_cnt = useAppSelector((state) => getAvailabilityCount(state)); const buckets = useAppSelector((state) => state.app.currentBuckets); @@ -739,12 +703,12 @@ const SurveyEnter = () => { const isLow = useAppSelector((state) => isLowBalance(state)); useEffect(() => { - if (!bpuid) return; + if (!user?.product_user_id) return; new OfferwallApi() .topNPlusBlockOfferwallProductIdOfferwallD48cce47Get( bpid, // productId - bpuid, // bpuid + user.product_user_id, undefined, // ip undefined, // countryIso undefined, // languages @@ -766,7 +730,7 @@ const SurveyEnter = () => { dispatch(setCurrentBuckets(top_n_res.buckets)); dispatch(setOfferwallReasons(top_n_res.offerwall_reasons ?? [])); }); - }, [loi, bpuid]); + }, [loi, user?.product_user_id]); const start_survey = () => { if (!bucket?.uri) return; @@ -834,7 +798,6 @@ type RequestState = "idle" | "working" | "sent" | "failed"; const Login = () => { const [email, setEmail] = useState(""); const [state, setState] = useState("idle"); - const bpuid = useAppSelector((state) => state.app.bpuid); // const dispatch = useAppDispatch(); const submit = async (event: FormEvent) => { diff --git a/jb-ui/src/types/authApi.ts b/jb-ui/src/types/authApi.ts new file mode 100644 index 0000000..19aeee0 --- /dev/null +++ b/jb-ui/src/types/authApi.ts @@ -0,0 +1,6 @@ +export interface User { + email: string; + display_name: string | null; + blocked: boolean; + product_user_id: string; +} -- cgit v1.2.3 From 83821dff9984fb950e53f23baf99d7a74fb88abd Mon Sep 17 00:00:00 2001 From: Max Nanis Date: Sun, 13 Sep 2026 10:41:49 -0700 Subject: refrences to HIT removed. Still can't test. Basic CashoutMethod retrival until backup updated. use of the new Wallets endpoint. --- jb-ui/src/JBApp.tsx | 69 +++++++++++++++++++++++++++------ jb-ui/src/components/Redemption.tsx | 67 ++++++++++++++++++++++++++++++++ jb-ui/src/components/Wallet.tsx | 20 ++++++---- jb-ui/src/models/app.ts | 8 ++++ jb-ui/src/models/appSlice.ts | 30 ++++++++++++++ jb-ui/src/models/cashoutMethodsSlice.ts | 37 ++++++++++++++++++ jb-ui/src/pages/Preview.tsx | 8 ++-- jb-ui/src/pages/Result.tsx | 18 +++------ jb-ui/src/pages/Work.tsx | 33 +++++++--------- jb-ui/src/store.ts | 2 + 10 files changed, 239 insertions(+), 53 deletions(-) create mode 100644 jb-ui/src/components/Redemption.tsx create mode 100644 jb-ui/src/models/cashoutMethodsSlice.ts (limited to 'jb-ui/src/components') diff --git a/jb-ui/src/JBApp.tsx b/jb-ui/src/JBApp.tsx index 6c31164..4870e9b 100644 --- a/jb-ui/src/JBApp.tsx +++ b/jb-ui/src/JBApp.tsx @@ -11,7 +11,11 @@ import { TaskStatusResponse, UserWalletBalance, UserWalletBalanceResponse, + UserLedgerWallets, + UserLedgerWalletAccountTypeEnum, + UserDisplayedWalletBalance, WalletApi, + CashoutMethodsResponse, } from "@/api_fsb"; import Footer from "@/components/Footer"; import { useAppDispatch } from "@/hooks"; @@ -20,8 +24,13 @@ import { useGetMeQuery } from "@/models/authAPI"; import { setAssignmentID, setProductUserID, - setTurkSubmitTo, + setUserDisplayedWalletBalance, + setUserWallet, + setUserAttemptCreditWallet, } from "@/models/appSlice"; + +import { addCashoutMethods } from "@/models/cashoutMethodsSlice"; + import { addEvent } from "@/models/grlEventsSlice"; import { addStatsData } from "@/models/grlStatsSlice"; import Home from "@/pages/Home"; @@ -50,6 +59,7 @@ import Wallet from "@/components/Wallet"; import "@/index.css"; import { setTaskStatus, setUserWalletBalance } from "@/models/appSlice"; import { Profiling } from "./components/Profiling"; +import Redemption from "./components/Redemption"; type Message = | PingMessage @@ -76,17 +86,16 @@ function QueryParamProcessor() { const { data: user, isLoading, isError } = useGetMeQuery(); useEffect(() => { - const assignment_id = searchParams.get("assignmentId"); - const tsid = searchParams.get("tsid"); - const worker_id = searchParams.get("workerId"); - - if (worker_id) { - dispatch(setProductUserID(worker_id)); - } - if (user?.product_user_id) { dispatch(setProductUserID(user.product_user_id)); + new WalletApi() + .getCashoutMethodsProductIdCashoutMethodsGet(bpid, user.product_user_id) + .then((res) => { + const response = res.data as CashoutMethodsResponse; + dispatch(addCashoutMethods(response.cashout_methods)); + }); + new WalletApi() .getUserWalletBalanceProductIdWalletGet( bpid, // productId @@ -97,6 +106,43 @@ function QueryParamProcessor() { const balance = response.wallet as UserWalletBalance; dispatch(setUserWalletBalance(balance)); }); + + new WalletApi() + .getUserWalletsProductIdWalletsGet(bpid, user.product_user_id) + .then((res) => { + const response = res.data as UserLedgerWallets; + + if (response.displayed_balances) { + setUserDisplayedWalletBalance(response.displayed_balances[0]); + } + + const userWallet = response.wallets?.find( + (w) => + w.account_type === UserLedgerWalletAccountTypeEnum.UserWallet, + ); + if (userWallet) { + setUserWallet(userWallet); + } + + const userAttemptCreditWallet = response.wallets?.find( + (w) => + w.account_type === + UserLedgerWalletAccountTypeEnum.UserAttemptCredit, + ); + if (userAttemptCreditWallet) { + setUserAttemptCreditWallet(userAttemptCreditWallet); + } + }); + } + }, [user]); + + useEffect(() => { + const assignment_id = searchParams.get("assignmentId"); + const tsid = searchParams.get("tsid"); + const worker_id = searchParams.get("workerId"); + + if (worker_id) { + dispatch(setProductUserID(worker_id)); } if (assignment_id) { @@ -125,8 +171,8 @@ function QueryParamProcessor() { const Layout = () => { const location = useLocation(); - const isWork = useMatch("/work*"); - const isPreview = useMatch("/preview*"); + const isWork = useMatch("/work/*"); + const isPreview = useMatch("/preview/*"); const isSpecialSubview = Boolean(isWork || isPreview); return ( @@ -152,6 +198,7 @@ const Layout = () => { > +
    diff --git a/jb-ui/src/components/Redemption.tsx b/jb-ui/src/components/Redemption.tsx new file mode 100644 index 0000000..0a91ab3 --- /dev/null +++ b/jb-ui/src/components/Redemption.tsx @@ -0,0 +1,67 @@ +import { clsx } from "clsx"; +import { useAppDispatch, useAppSelector } from "@/hooks"; +import { UpkQuestionChoice } from "@/api_fsb"; +import { useGetMeQuery } from "@/models/authAPI"; +import { ChevronRightIcon } from "lucide-react"; +import { KeyboardEvent, MouseEvent, useEffect, useRef, useState } from "react"; +import { useSelector } from "react-redux"; +import { cashoutMethodsSelectors } from "@/models/cashoutMethodsSlice"; + +const RedemptionHeader = () => { + const cashout_methods = useSelector(cashoutMethodsSelectors.selectAll); + + return ( + <> +

    + {/* {cashout_methods.length} Redemption Methods */} + 30+ Redemption Methods +

    + + ); +}; + +const RedemptionModalBody = () => { + return ( +
    +
    + Paypal, Visa, Giftcards will be available. Finishing processing the + transfer from Amazon. +
    +
    + ); +}; + +const Redemption = () => { + const dispatch = useAppDispatch(); + const { data: user, isLoading, isError } = useGetMeQuery(); + const [showRedemption, setShowRedemption] = useState(false); + + return ( +
    +
    setShowRedemption(!showRedemption)} + > + + +

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

    +
    + + {showRedemption && } +
    + ); +}; + +export default Redemption; diff --git a/jb-ui/src/components/Wallet.tsx b/jb-ui/src/components/Wallet.tsx index 3f5c6a4..3bb23a9 100644 --- a/jb-ui/src/components/Wallet.tsx +++ b/jb-ui/src/components/Wallet.tsx @@ -54,7 +54,13 @@ const getBackgroundColor = (balance: number | undefined): string => { const WalletHeader = () => { const amount = - useAppSelector((state) => state.app.userWalletBalance)?.amount ?? 0; + useAppSelector((state) => state.app.userDisplayedWalletBalance?.amount) ?? + 0; + + const amount_string = + useAppSelector( + (state) => state.app.userDisplayedWalletBalance?.amount_string, + ) ?? "$0.00"; const bgStyle = typeof getBackgroundColor(amount) === "string" && @@ -77,13 +83,9 @@ const WalletHeader = () => { User Wallet: {amount >= 0 ? ( - - {`${formatCentsToUSD(Math.abs(amount))}`} - + {`${amount_string}`} ) : ( - - {`(${formatCentsToUSD(Math.abs(amount))})`} - + {`(${amount_string})`} )}
    @@ -93,6 +95,10 @@ const WalletHeader = () => { const WalletSummary = () => { const summary = useAppSelector((state) => state.app.userLedgerSummary); + const userWallet = useAppSelector((state) => state.app.userWallet); + + userWallet; + const survey_completes = summary?.bp_payment?.entry_count ?? 0; const survey_completes_f = (survey_completes ?? 0).toLocaleString("en-US"); diff --git a/jb-ui/src/models/app.ts b/jb-ui/src/models/app.ts index 4c3e69e..db54e0d 100644 --- a/jb-ui/src/models/app.ts +++ b/jb-ui/src/models/app.ts @@ -5,6 +5,8 @@ import { UserLedgerTransactionTypesSummary, UserLedgerTransactionsResponseTransactionsInner, OfferwallReason, + UserLedgerWallet, + UserDisplayedWalletBalance, } from "@/api_fsb"; import { PaginationState } from "@tanstack/react-table"; @@ -33,6 +35,12 @@ export interface App { userLedgerTxCount?: number; userLedgerTxs: UserLedgerTransactionsResponseTransactionsInner[]; + // --- new approach + userDisplayedWalletBalance?: UserDisplayedWalletBalance; + userWallet?: UserLedgerWallet; + userAttemptCreditWallet?: UserLedgerWallet; + // --- + txPagination: PaginationState; txTotalItems?: number; txTotalPages?: number; diff --git a/jb-ui/src/models/appSlice.ts b/jb-ui/src/models/appSlice.ts index 46c6a2f..42a776e 100644 --- a/jb-ui/src/models/appSlice.ts +++ b/jb-ui/src/models/appSlice.ts @@ -4,6 +4,8 @@ import { UserLedgerTransactionsResponseTransactionsInner, UserLedgerTransactionTypesSummary, UserWalletBalance, + UserDisplayedWalletBalance, + UserLedgerWallet, OfferwallReason, } from "@/api_fsb"; import { App } from "@/models/app"; @@ -26,6 +28,12 @@ const initialState: App = { userLedgerTxCount: undefined, userLedgerTxs: [], + // --- new approach + userDisplayedWalletBalance: undefined, + userWallet: undefined, + userAttemptCreditWallet: undefined, + // --- + txPagination: { pageIndex: 0, pageSize: 10 }, txTotalItems: undefined, txTotalPages: undefined, @@ -91,6 +99,24 @@ const appSlice = createSlice({ state.userWalletBalance = action.payload; }, + // --- New approach --- + setUserDisplayedWalletBalance( + state, + action: PayloadAction, + ) { + state.userDisplayedWalletBalance = action.payload; + }, + + setUserWallet(state, action: PayloadAction) { + state.userWallet = action.payload; + }, + + setUserAttemptCreditWallet(state, action: PayloadAction) { + state.userAttemptCreditWallet = action.payload; + }, + + // --- --- + setUserLedgerSummary( state, action: PayloadAction, @@ -138,6 +164,10 @@ export const { setUserLedgerSummary, setUserLedgerTxs, + setUserDisplayedWalletBalance, + setUserWallet, + setUserAttemptCreditWallet, + setTxPagination, setTxTotalItems, setTxTotalPages, diff --git a/jb-ui/src/models/cashoutMethodsSlice.ts b/jb-ui/src/models/cashoutMethodsSlice.ts new file mode 100644 index 0000000..92c03da --- /dev/null +++ b/jb-ui/src/models/cashoutMethodsSlice.ts @@ -0,0 +1,37 @@ +import { CashoutMethodOut } from "@/api_fsb"; +import type { RootState } from "@/store"; +import { createSlice, createEntityAdapter } from "@reduxjs/toolkit"; + +// Entity adapter for normalized storage +const cashoutMethodsAdapter = createEntityAdapter({ + selectId: (model: CashoutMethodOut) => model.id!, + sortComparer: (a, b) => { + return (b.min_value ?? 0) - (a.min_value ?? 0); + }, +}); + +const cashoutMethodsSlice = createSlice({ + name: "cashoutMethods", + initialState: cashoutMethodsAdapter.getInitialState(), + + reducers: { + addCashoutMethod: cashoutMethodsAdapter.addOne, + addCashoutMethods: cashoutMethodsAdapter.addMany, + upsertCashoutMethod: cashoutMethodsAdapter.upsertOne, // Add or Update + }, +}); + +export const { addCashoutMethod, addCashoutMethods, upsertCashoutMethod } = + cashoutMethodsSlice.actions; + +export const cashoutMethodsSelectors = cashoutMethodsAdapter.getSelectors( + (state: RootState) => state.cashoutMethods, +); + +export const selectAllCashoutMethods = ( + state: RootState, +): CashoutMethodOut[] => { + return cashoutMethodsSelectors.selectAll(state) ?? []; +}; + +export default cashoutMethodsSlice.reducer; diff --git a/jb-ui/src/pages/Preview.tsx b/jb-ui/src/pages/Preview.tsx index 85e49f2..900621e 100644 --- a/jb-ui/src/pages/Preview.tsx +++ b/jb-ui/src/pages/Preview.tsx @@ -378,10 +378,10 @@ const Preview = () => { Get paid to take surveys. Our algorithm attempts to pair you with the highest paying survey available at the moment (right now, the highest paying survey is {survey_max_cpi_f}) that you are eligible - for, each attempt is a valid HIT. Try as many as you can, our system - will self regulate you: we want to ensure you have a high chance of - being paired with a survey based on what surveys are available, as - their availability constantly changes. + for. Try as many as you can, our system will self regulate you: we + want to ensure you have a high chance of being paired with a survey + based on what surveys are available, as their availability + constantly changes.

    diff --git a/jb-ui/src/pages/Result.tsx b/jb-ui/src/pages/Result.tsx index 477f77e..6385bc7 100644 --- a/jb-ui/src/pages/Result.tsx +++ b/jb-ui/src/pages/Result.tsx @@ -21,9 +21,7 @@ export const Complete = () => {

    {ts?.user_payout_string}

    -

    - You may close this tab and return to submit the HIT -

    +

    You may close this tab

    ); }; @@ -35,9 +33,7 @@ export const Enter = () => {

    You're currently in the Survey

    -

    - You may close this tab and return to submit the HIT -

    +

    You may close this tab

    ); }; @@ -50,13 +46,11 @@ export const InComplete = () => { return ( <> -

    HIT Complete

    +

    Attempt Complete

    Survey terminated the attempt.

    -

    - You may close this tab and return to submit the HIT -

    +

    You may close this tab

    ); }; @@ -68,9 +62,7 @@ export const Unknown = () => {

    You're not currently eligible for any Surveys

    -

    - You may close this tab and return to submit the HIT -

    +

    You may close this tab

    ); }; diff --git a/jb-ui/src/pages/Work.tsx b/jb-ui/src/pages/Work.tsx index dbe3601..50ed930 100644 --- a/jb-ui/src/pages/Work.tsx +++ b/jb-ui/src/pages/Work.tsx @@ -1,4 +1,5 @@ import { useGetMeQuery } from "@/models/authAPI"; +import { Link } from "react-router-dom"; import { BucketTask, OfferwallApi, @@ -183,7 +184,7 @@ const Report = ({ onClose }: { onClose: () => void }) => { hover:cursor-pointer hover:bg-rose-400 hover:text-rose-50" onClick={clicked_submit} > - Submit and Complete HIT + Submit and Complete Attempt
    @@ -306,7 +307,7 @@ const SurveyMonitor = () => { } // This is if they have finished a survey complete and it resulted in a - // COMPLETE or a INCOMPLETE (doesn't matter). They can now submit the HIT. + // COMPLETE or a INCOMPLETE (doesn't matter). They can now submit the Attempt. // // 2 - INCOMPLETE, 3 - COMPLETE return ( @@ -326,7 +327,7 @@ const SurveyMonitor = () => { py-2 text-zinc-500 text-md font-medium text-center" > - You may now submit the HIT. + You may now submit the attempt.

    )} @@ -351,7 +352,9 @@ const SurveyMonitor = () => { pt-0 pb-4 text-red-950 text-sm font-semibold text-center" > - Please return the HIT to avoid having your assignment rejected. + + Click to Refresh and Try Again +

    @@ -368,7 +371,7 @@ const SurveyMonitor = () => { hover:cursor-pointer hover:bg-emerald-400 hover:text-emerald-50" onClick={() => setShowComplete(true)} > - Submit HIT + Finish Attempt