aboutsummaryrefslogtreecommitdiff
path: root/jb-ui/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'jb-ui/src/components')
-rw-r--r--jb-ui/src/components/EventMarquee.tsx428
-rw-r--r--jb-ui/src/components/Footer.tsx76
-rw-r--r--jb-ui/src/components/Profiling.tsx1297
-rw-r--r--jb-ui/src/components/Wallet.tsx859
-rw-r--r--jb-ui/src/components/ui/button.tsx20
-rw-r--r--jb-ui/src/components/ui/checkbox.tsx14
-rw-r--r--jb-ui/src/components/ui/confetti.tsx98
-rw-r--r--jb-ui/src/components/ui/dot-pattern.tsx60
-rw-r--r--jb-ui/src/components/ui/field.tsx72
-rw-r--r--jb-ui/src/components/ui/label.tsx12
-rw-r--r--jb-ui/src/components/ui/magic-card.tsx70
-rw-r--r--jb-ui/src/components/ui/pagination.tsx30
-rw-r--r--jb-ui/src/components/ui/separator.tsx14
-rw-r--r--jb-ui/src/components/ui/skeleton.tsx6
14 files changed, 1571 insertions, 1485 deletions
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 (
- <>
- <span className="px-2 text-lg">{getUnicodeFlagIcon(payload.country_iso)}</span>
- <p>
- {truncate(event.product_user_id!, 6, "*****")} Entered{" "}
- {formatSource(payload.source)}{" "}
- (#{truncate(payload.survey_id!, 5, "***")})
- </p>
- </>
- )
-}
+ const payload = event.payload as TaskEnterPayload;
+
+ return (
+ <>
+ <span className="px-2 text-lg">
+ {getUnicodeFlagIcon(payload.country_iso)}
+ </span>
+ <p>
+ {truncate(event.product_user_id!, 6, "*****")} Entered{" "}
+ {formatSource(payload.source)} (#
+ {truncate(payload.survey_id!, 5, "***")})
+ </p>
+ </>
+ );
+};
const EventTaskFinish = ({ event }: EventItemProps) => {
- const payload = event.payload as TaskFinishPayload;
-
- return (
- <>
- <span className="px-2 text-lg">{getUnicodeFlagIcon(event.payload.country_iso)}</span>
- <p>
- {truncate(event.product_user_id!, 6, "*****")}{" "}
- {formatStatus(payload.status)} {formatSource(payload.source)}{" "}
- (#{truncate(payload.survey_id!, 5, "***")}) in{" "}
- {formatSecondsVerbose(payload.duration_sec!)}
- </p>
- </>
- )
-}
+ const payload = event.payload as TaskFinishPayload;
+
+ return (
+ <>
+ <span className="px-2 text-lg">
+ {getUnicodeFlagIcon(event.payload.country_iso)}
+ </span>
+ <p>
+ {truncate(event.product_user_id!, 6, "*****")}{" "}
+ {formatStatus(payload.status)} {formatSource(payload.source)} (#
+ {truncate(payload.survey_id!, 5, "***")}) in{" "}
+ {formatSecondsVerbose(payload.duration_sec!)}
+ </p>
+ </>
+ );
+};
const EventUserCreated = ({ event }: EventItemProps) => {
-
- return (
- <>
- <span className="px-2 text-lg">{getUnicodeFlagIcon(event.payload.country_iso)}</span>
- <p>{truncate(event.product_user_id!, 6, "*****")} Created</p>
- </>
- )
-}
-
+ return (
+ <>
+ <span className="px-2 text-lg">
+ {getUnicodeFlagIcon(event.payload.country_iso)}
+ </span>
+ <p>{truncate(event.product_user_id!, 6, "*****")} Created</p>
+ </>
+ );
+};
const EventComponent = ({ event }: EventItemProps) => {
-
- const renderContent = () => {
- switch (event.event_type) {
- case EventType.TaskEnter:
- return <EventTaskEnter event={event} />
- case EventType.TaskFinish:
- return <EventTaskFinish event={event} />
- case EventType.UserCreated:
- return <EventUserCreated event={event} />
- default:
- return <><p>Unknown event</p></>
- }
+ const renderContent = () => {
+ switch (event.event_type) {
+ case EventType.TaskEnter:
+ return <EventTaskEnter event={event} />;
+ case EventType.TaskFinish:
+ return <EventTaskFinish event={event} />;
+ case EventType.UserCreated:
+ return <EventUserCreated event={event} />;
+ default:
+ return (
+ <>
+ <p>Unknown event</p>
+ </>
+ );
}
+ };
- return (
- <div key={event.event_uuid!}
- className="inline-flex items-center text-[10px]
- font-mono px-2">
- {renderContent()}
-
- {/* <span className="text-red-500 pl-5">◆</span> */}
- </div >
- )
-}
+ return (
+ <div
+ key={event.event_uuid!}
+ className="inline-flex items-center text-[10px]
+ font-mono px-2"
+ >
+ {renderContent()}
+ {/* <span className="text-red-500 pl-5">◆</span> */}
+ </div>
+ );
+};
// ─── 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<HTMLDivElement | null>(null); // The ticker band
- const stripRef = useRef<HTMLDivElement | null>(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<number | null>(null);
- const lastTime = useRef<number | null>(null);
-
- // --- Item tracking ---
- // Track which IDs are already in the scroller to only append
- // genuinely new ones.
- const seenUUIDs = useRef(new Set<string>());
- const pendingQueue = useRef<EventEnvelope[]>([]);
- const [scrollItems, setScrollItems] = useState<EventEnvelope[]>([]);
-
- // --- 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 (
- <div style={{
- minHeight: "3vh", display: "flex", flexDirection: "column",
- justifyContent: "center", alignItems: "center", gap: "0"
- }}>
-
- {/* Ticker wrapper */}
- <div style={{ width: "100%", position: "relative" }}>
-
- {/* The ticker band */}
- <div
- ref={containerRef}
- style={{
- overflow: "hidden",
- padding: "0",
- position: "relative",
- }}
- >
-
- {/* Scrolling strip */}
- <div
- ref={stripRef}
- style={{
- display: "inline-flex",
- alignItems: "center",
- whiteSpace: "nowrap",
- padding: "10px 0",
- willChange: "transform",
- position: "relative",
- zIndex: 2,
- }}
- >
- {scrollItems.map(item => (
- <EventComponent event={item} />
- ))}
-
- {/* Trailing spacer so new content doesn't immediately snap in */}
- <span style={{ paddingRight: "100vw", display: "inline-block" }} />
-
- </div>
- </div>
-
- </div>
+ // --- Redux ---
+ const reduxItems = useSelector(selectAllEvents);
+ const targetSpeedRedux = useSelector(selectCurrentSpeed); // px/sec
+
+ // --- Scroll state (all refs — no re-renders from animation) ---
+ const containerRef = useRef<HTMLDivElement | null>(null); // The ticker band
+ const stripRef = useRef<HTMLDivElement | null>(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<number | null>(null);
+ const lastTime = useRef<number | null>(null);
+
+ // --- Item tracking ---
+ // Track which IDs are already in the scroller to only append
+ // genuinely new ones.
+ const seenUUIDs = useRef(new Set<string>());
+ const pendingQueue = useRef<EventEnvelope[]>([]);
+ const [scrollItems, setScrollItems] = useState<EventEnvelope[]>([]);
+
+ // --- 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 (
+ <div
+ style={{
+ minHeight: "3vh",
+ display: "flex",
+ flexDirection: "column",
+ justifyContent: "center",
+ alignItems: "center",
+ gap: "0",
+ }}
+ >
+ {/* Ticker wrapper */}
+ <div style={{ width: "100%", position: "relative" }}>
+ {/* The ticker band */}
+ <div
+ ref={containerRef}
+ style={{
+ overflow: "hidden",
+ padding: "0",
+ position: "relative",
+ }}
+ >
+ {/* Scrolling strip */}
+ <div
+ ref={stripRef}
+ style={{
+ display: "inline-flex",
+ alignItems: "center",
+ whiteSpace: "nowrap",
+ padding: "10px 0",
+ willChange: "transform",
+ position: "relative",
+ zIndex: 2,
+ }}
+ >
+ {scrollItems.map((item) => (
+ <EventComponent event={item} />
+ ))}
+
+ {/* Trailing spacer so new content doesn't immediately snap in */}
+ <span style={{ paddingRight: "100vw", display: "inline-block" }} />
+ </div>
</div>
- );
+ </div>
+ </div>
+ );
}
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 (
- <div className="grid gap-1 grid-cols-1
+ return (
+ <div
+ className="grid gap-1 grid-cols-1
md:grid-cols-3
- text-xs">
- <div>
- <p>help: <a
- href={`mailto:support@jamesbillings67.com?subject=AMT Support Request: (${(bpuid ?? '-')}) ${(assignment_id ?? '–')}`}>
- support@jamesbillings67.com
- </a>
- </p>
- </div>
+ text-xs"
+ >
+ <div>
+ <p>
+ help:{" "}
+ <a
+ href={`mailto:support@jamesbillings67.com?subject=AMT Support Request: (${bpuid ?? "-"}) ${assignment_id ?? "–"}`}
+ >
+ support@jamesbillings67.com
+ </a>
+ </p>
+ </div>
- <div>
- <p>made with <span className="text-rose-500">♥</span> by <a
- className="hover:cursor-pointer hover:text-pink"
- href="https://instagram.com/x0xMaximus"
- target="_blank">Max Nanis</a>
- </p>
- </div>
+ <div>
+ <p>
+ made with <span className="text-rose-500">♥</span> by{" "}
+ <a
+ className="hover:cursor-pointer hover:text-pink"
+ href="https://instagram.com/x0xMaximus"
+ target="_blank"
+ >
+ Max Nanis
+ </a>
+ </p>
+ </div>
- <div>
- <p>
- <a href="https://git.generalresearch.com/panels/amt-jb/"
- target="_blank">
- open source
- </a>
- </p>
- </div>
+ <div>
+ <p>
+ <a
+ href="https://git.generalresearch.com/panels/amt-jb/"
+ target="_blank"
+ >
+ open source
+ </a>
+ </p>
+ </div>
+ </div>
+ );
+};
- </div>
- )
-
-}
-
-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<HTMLInputElement>) => {
- 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<HTMLInputElement>) => {
- if (!activeQuestion.question_id) return;
- if (!bpuid) return;
-
- if (event.key === 'Enter') {
- dispatch(saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid }))
- }
- };
-
- return (
- <div className="px-6 my-6">
- <input type="text"
- id="text-entry-input"
- aria-describedby=""
- // defaultValue={answer?.values.length ? answer?.values[0] : ""}
- onKeyUp={handleInputChange}
- onKeyDown={handleKeyPress} // Use onKeyDown to listen for the key press
- // title={error ? answer?.error_msg : ""}
- className={clsx(
- 'w-full border-1 rounded px-2 py-1',
- error ?
- 'border-red-500 focus-visible:ring-red-500' :
- 'border-blue-50'
- )}
- />
-
- {
- validation.errors.map(e => {
- return (
- <p className="text-sm text-red-100 my-2">{e.message}</p>
- )
- })
- }
- </div>
- )
-}
-
-
-export default function useKeyboardMode(
- rowModel: RowModel<UpkQuestionChoice>,
-) {
- // 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<HTMLInputElement>) => {
+ 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<HTMLInputElement>) => {
+ if (!activeQuestion.question_id) return;
+ if (!bpuid) return;
+
+ if (event.key === "Enter") {
+ dispatch(
+ saveAnswer({ questionId: activeQuestion.question_id!, bpuid: bpuid }),
+ );
+ }
+ };
+
+ return (
+ <div className="px-6 my-6">
+ <input
+ type="text"
+ id="text-entry-input"
+ aria-describedby=""
+ // defaultValue={answer?.values.length ? answer?.values[0] : ""}
+ onKeyUp={handleInputChange}
+ onKeyDown={handleKeyPress} // Use onKeyDown to listen for the key press
+ // title={error ? answer?.error_msg : ""}
+ className={clsx(
+ "w-full border-1 rounded px-2 py-1",
+ error
+ ? "border-red-500 focus-visible:ring-red-500"
+ : "border-blue-50",
+ )}
+ />
- const { key } = event;
- const totalRows = rowRefs.current.length;
- const prevIdx = keyboardSelectionIdxRef.current;
- let nextIdx = prevIdx;
+ {validation.errors.map((e) => {
+ return <p className="text-sm text-red-100 my-2">{e.message}</p>;
+ })}
+ </div>
+ );
+};
- 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<UpkQuestionChoice>) {
+ // 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<RowSelectionState>({});
- 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<RowSelectionState>({});
+ 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<UpkQuestionChoice>();
+ 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<UpkQuestionChoice>()
- const columns = [
- columnHelper.display({
- id: 'select',
- cell: ({ row }) => {
- const canSelected = row.getCanSelect()
-
- return (
- <input
- type="checkbox"
- checked={row.getIsSelected()}
- disabled={!row.getCanSelect()}
- onChange={row.getToggleSelectedHandler()}
- className={`w-4 h-4 rounded-0 border-slate-300 cursor-pointer
+ return (
+ <input
+ type="checkbox"
+ checked={row.getIsSelected()}
+ disabled={!row.getCanSelect()}
+ onChange={row.getToggleSelectedHandler()}
+ className={`w-4 h-4 rounded-0 border-slate-300 cursor-pointer
focus:outline-none focus:border-blue-500 focus:ring-blue-500
- ${canSelected ? ' ' : 'hidden'}
+ ${canSelected ? " " : "hidden"}
`}
- />
- )
- },
- 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 (
- <p className={`
+ />
+ );
+ },
+ 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 (
+ <p
+ className={`
w-max truncate
text-sm text-left pl-1
- ${selected ? 'font-bold' : 'font-medium'}
- ${canSelected ? 'text-blue-950' : 'text-blue-600'}
- `}>
- {text}
- </p>
- )
- },
- 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 */}
- <div className="py-1">
- <div className="flex flex-wrap gap-2">
-
- {/* Search */}
- <div className="flex-1">
- <input
- type="text"
- placeholder="Search choices..."
- value={globalFilter ?? ''}
- onChange={e => setGlobalFilter(e.target.value)}
- className="w-full px-4 py-1
+ ${selected ? "font-bold" : "font-medium"}
+ ${canSelected ? "text-blue-950" : "text-blue-600"}
+ `}
+ >
+ {text}
+ </p>
+ );
+ },
+ 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 */}
+ <div className="py-1">
+ <div className="flex flex-wrap gap-2">
+ {/* Search */}
+ <div className="flex-1">
+ <input
+ type="text"
+ placeholder="Search choices..."
+ value={globalFilter ?? ""}
+ onChange={(e) => 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"
- />
- </div>
+ />
+ </div>
- {/* Selected Count */}
- {selectedCount > 0 && (
- <div className="flex items-center gap-2 px-2 py-1
+ {/* Selected Count */}
+ {selectedCount > 0 && (
+ <div
+ className="flex items-center gap-2 px-2 py-1
border border-blue-900 text-blue-700 rounded font-medium
text-[10px]
hover:cursor-pointer hover:bg-blue-800 hover:text-white
"
- onClick={() => setRowSelection({})}>
- <span>{selectedCount} selected</span>
- <button className="m-0 p-0 font-bold">✕</button>
- </div>
- )}
-
- </div>
+ onClick={() => setRowSelection({})}
+ >
+ <span>{selectedCount} selected</span>
+ <button className="m-0 p-0 font-bold">✕</button>
</div>
+ )}
+ </div>
+ </div>
- {/* Table */}
- <div className="border border-blue-200 rounded max-h-[400px] overflow-y-auto
+ {/* Table */}
+ <div
+ className="border border-blue-200 rounded max-h-[400px] overflow-y-auto
[&::-webkit-scrollbar]:h-[4px]
[&::-webkit-scrollbar]:w-1
[&::-webkit-scrollbar-track]:bg-blue-100
[&::-webkit-scrollbar-thumb]:bg-blue-300
- ">
- <table className="text-xs table-fixed border-collapse w-full">
- <tbody className="w-full">
- {table.getRowModel().rows.map((row, idx) => (
- <tr key={row.id}
- ref={(el) => {
- rowRefs.current[idx] = el;
- }}
- onClick={() => row.toggleSelected()}
- className={`cursor-pointer transition-colors w-full
- ${row.getIsSelected() ? 'bg-blue-600 ' : ' '
+ "
+ >
+ <table className="text-xs table-fixed border-collapse w-full">
+ <tbody className="w-full">
+ {table.getRowModel().rows.map((row, idx) => (
+ <tr
+ key={row.id}
+ ref={(el) => {
+ 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 (
- <td key={cell.id}
- className={`py-2 ${alignClass}`}
- style={{
- width: `${cell.column.getSize()}px`,
- }}
- >
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
- </td>
- )
- })}
-
- </tr>
- ))}
- </tbody>
- </table>
- </div>
-
- {/* Empty State */}
- {table.getRowModel().rows.length === 0 && (
- <div className="p-12 text-center text-blue-50 font-bold">
- No items found matching your search
- </div>
- )}
+ >
+ {row.getVisibleCells().map((cell) => {
+ const align = cell.column.columnDef.meta?.align || "left";
+ const alignClass =
+ align === "center"
+ ? "text-center"
+ : align === "right"
+ ? "text-right"
+ : "text-left";
+
+ return (
+ <td
+ key={cell.id}
+ className={`py-2 ${alignClass}`}
+ style={{
+ width: `${cell.column.getSize()}px`,
+ }}
+ >
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext(),
+ )}
+ </td>
+ );
+ })}
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+
+ {/* Empty State */}
+ {table.getRowModel().rows.length === 0 && (
+ <div className="p-12 text-center text-blue-50 font-bold">
+ No items found matching your search
+ </div>
+ )}
- {/* Footer */}
- <div className="px-2 py-2 flex justify-between items-center
+ {/* Footer */}
+ <div
+ className="px-2 py-2 flex justify-between items-center
text-xs font-mono font-semibold text-blue-950
- ">
- <span>
- Showing {table.getRowModel().rows.length} of {activeQuestion?.choices?.length} choices
- </span>
- {selectedCount > 0 && (
- <span>
- {selectedCount} choice{selectedCount !== 1 ? 's' : ''} selected
- </span>
- )}
- </div>
-
- </>
- )
-
-}
+ "
+ >
+ <span>
+ Showing {table.getRowModel().rows.length} of{" "}
+ {activeQuestion?.choices?.length} choices
+ </span>
+ {selectedCount > 0 && (
+ <span>
+ {selectedCount} choice{selectedCount !== 1 ? "s" : ""} selected
+ </span>
+ )}
+ </div>
+ </>
+ );
+};
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 (
- <div className="flex justify-between items-center px-2">
-
- <h3 className="text-left text-sm font-bold m-0 p-0
+ return (
+ <div className="flex justify-between items-center px-2">
+ <h3
+ className="text-left text-sm font-bold m-0 p-0
inline-block
- ">
- {text}
- </h3>
-
- <div className="max-w-[70px] text-center m-0 p-0">
- <h4 className="text-xs font-bold">
- {question_active_tasks.toLocaleString()}
- </h4>
- <h5 className="text-[10px]">
- Surveys use
- this question
- </h5>
- </div>
- </div>
- )
-}
+ "
+ >
+ {text}
+ </h3>
+
+ <div className="max-w-[70px] text-center m-0 p-0">
+ <h4 className="text-xs font-bold">
+ {question_active_tasks.toLocaleString()}
+ </h4>
+ <h5 className="text-[10px]">Surveys use this question</h5>
+ </div>
+ </div>
+ );
+};
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 (
- <button
- type="submit"
- className="cursor-pointer text-sm font-semibold
+ 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 (
+ <button
+ type="submit"
+ className="cursor-pointer text-sm font-semibold
bg-blue-700 text-white py-1 px-4 rounded
hover:bg-blue-900 min-w-[148px]
disabled:bg-blue-700 disabled:text-rose-300
disabled:cursor-not-allowed
"
- disabled={!activeQuestionValidation?.isComplete}
- onClick={handleOnClick}
- >
- Answer Question
- </button>
- )
-}
-
+ disabled={!activeQuestionValidation?.isComplete}
+ onClick={handleOnClick}
+ >
+ Answer Question
+ </button>
+ );
+};
export const ProfileQuestionFull = () => {
- const activeQuestion = useSelector(selectActiveQuestion);
-
- const renderContent = () => {
- if (!activeQuestion) return null;
- switch (activeQuestion.question_type) {
- case 'TE':
- return <TextEntry />
- case 'MC':
- return <MultipleChoice />
- default:
- return <MultipleChoice />
- }
- };
+ const activeQuestion = useSelector(selectActiveQuestion);
+
+ const renderContent = () => {
+ if (!activeQuestion) return null;
+ switch (activeQuestion.question_type) {
+ case "TE":
+ return <TextEntry />;
+ case "MC":
+ return <MultipleChoice />;
+ default:
+ return <MultipleChoice />;
+ }
+ };
- if (!activeQuestion) return null
+ if (!activeQuestion) return null;
- return (
- <>
- {/* {answer && answer.processing && (
+ return (
+ <>
+ {/* {answer && answer.processing && (
<motion.div
className="bg-gray-300"
initial={{ width: "0%" }}
@@ -540,104 +566,111 @@ export const ProfileQuestionFull = () => {
/>
)} */}
- {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<HTMLAnchorElement>) => {
- if (completed) {
- evt.preventDefault()
- } else {
- dispatch(setActiveQuestion(question.question_id!))
- }
+ const handleSetQuestion = (evt: MouseEvent<HTMLAnchorElement>) => {
+ if (completed) {
+ evt.preventDefault();
+ } else {
+ dispatch(setActiveQuestion(question.question_id!));
}
-
- return (
- <PaginationItem>
- <PaginationLink
- href="#"
- title={question.question_text}
- isActive={isActive}
- aria-disabled={!!completed}
-
- onClick={handleSetQuestion}
- className={clsx("cursor-pointer rounded border border-blue-300 text-blue-300 text-[10px] w-7 h-4",
- {
- "pointer-events-none cursor-not-allowed": completed,
- "bg-blue-800 border-blue-800": isActive,
- })}
- >
- {idx + 1}
- </PaginationLink>
- </PaginationItem>
- )
-}
+ };
+
+ return (
+ <PaginationItem>
+ <PaginationLink
+ href="#"
+ title={question.question_text}
+ isActive={isActive}
+ aria-disabled={!!completed}
+ onClick={handleSetQuestion}
+ className={clsx(
+ "cursor-pointer rounded border border-blue-300 text-blue-300 text-[10px] w-7 h-4",
+ {
+ "pointer-events-none cursor-not-allowed": completed,
+ "bg-blue-800 border-blue-800": isActive,
+ },
+ )}
+ >
+ {idx + 1}
+ </PaginationLink>
+ </PaginationItem>
+ );
+};
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 (
- <Pagination>
- <PaginationContent>
- {
- questionsWindow(questions, q_idx).map(q => {
- return <PaginationIcon
- key={q.question_id}
- question={q}
- idx={questions.findIndex(qq => qq.question_id === q.question_id)}
- />
- })
- }
-
- <PaginationItem>
- <PaginationLink
- aria-label="Go to next page"
- size="default"
-
- className="rounded text-[10px] p-0 m-0
+ return items.slice(start, end);
+ };
+
+ return (
+ <Pagination>
+ <PaginationContent>
+ {questionsWindow(questions, q_idx).map((q) => {
+ return (
+ <PaginationIcon
+ key={q.question_id}
+ question={q}
+ idx={questions.findIndex(
+ (qq) => qq.question_id === q.question_id,
+ )}
+ />
+ );
+ })}
+
+ <PaginationItem>
+ <PaginationLink
+ aria-label="Go to next page"
+ size="default"
+ className="rounded text-[10px] p-0 m-0
text-blue-300
border border-blue-100 text-blue-100
hover:bg-blue-800 hover:border-blue-800 hover:text-white
@@ -645,115 +678,111 @@ const ProfilingQuestionPagination = () => {
h-4
w-13 sm:w-14
"
- onClick={handleNextClick}
- href="#"
- >
- <span className="block">Next</span>
- <ChevronRightIcon />
- </PaginationLink>
-
- </PaginationItem>
- </PaginationContent>
- </Pagination>
- )
-}
+ onClick={handleNextClick}
+ href="#"
+ >
+ <span className="block">Next</span>
+ <ChevronRightIcon />
+ </PaginationLink>
+ </PaginationItem>
+ </PaginationContent>
+ </Pagination>
+ );
+};
const ProfilingHeader = () => {
- const questions = useSelector(questionsSelectors.selectAll);
+ const questions = useSelector(questionsSelectors.selectAll);
- return (
- <h2 className="text-xs font-mono font-bold p-0 m-0">
- {questions.length} Profiling Questions
- </h2>
- )
-}
+ return (
+ <h2 className="text-xs font-mono font-bold p-0 m-0">
+ {questions.length} Profiling Questions
+ </h2>
+ );
+};
const ProfilingModalBody = () => {
- const activeQuestion = useSelector(selectActiveQuestion);
-
- if (!activeQuestion) {
- return (
- <div className="p-5">
- <h3 className="text-lg font-bold">No Profiling Questions Available</h3>
- <p className="my-0 text-sm text-gray-600">Please check back later.</p>
- </div>
- )
- }
+ const activeQuestion = useSelector(selectActiveQuestion);
+ if (!activeQuestion) {
return (
- <div className="relative flex flex-col">
- {/* <ProfilingQuestionPagination /> */}
-
- <div className="flex-shrink-0">
- <ProfilingQuestionTitle />
- </div>
-
- {/* Scrollable Content */}
- <div className="flex-1 p-1">
- <ProfileQuestionFull key={activeQuestion.question_id} />
- </div>
-
- {/* Sticky Footer */}
- <div className="flex-shrink-0 bg-blue-600 p-1 flex justify-between
- ">
- <div className="justify-start">
- <ProfilingQuestionPagination />
- </div>
- <div className="justify-end">
- <ProfilingQuestionSubmitButton />
- </div>
- </div>
-
+ <div className="p-5">
+ <h3 className="text-lg font-bold">No Profiling Questions Available</h3>
+ <p className="my-0 text-sm text-gray-600">Please check back later.</p>
+ </div>
+ );
+ }
+
+ return (
+ <div className="relative flex flex-col">
+ {/* <ProfilingQuestionPagination /> */}
+
+ <div className="flex-shrink-0">
+ <ProfilingQuestionTitle />
+ </div>
+
+ {/* Scrollable Content */}
+ <div className="flex-1 p-1">
+ <ProfileQuestionFull key={activeQuestion.question_id} />
+ </div>
+
+ {/* Sticky Footer */}
+ <div
+ className="flex-shrink-0 bg-blue-600 p-1 flex justify-between
+ "
+ >
+ <div className="justify-start">
+ <ProfilingQuestionPagination />
</div>
- )
-}
+ <div className="justify-end">
+ <ProfilingQuestionSubmitButton />
+ </div>
+ </div>
+ </div>
+ );
+};
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 (
- <div className={clsx(
- "fixed rounded bg-blue-500 text-white shadow-lg z-60 \
+ return (
+ <div
+ className={clsx(
+ "fixed rounded bg-blue-500 text-white shadow-lg z-60 \
bottom-1 sm:bottom-6 \
left-1 sm:left-2",
- showProfiling ? "w-[98vw] sm:w-[400px]" : "w-[220px]"
- )}
- >
-
- <div className="bg-blue-600 rounded border-white-300
+ showProfiling ? "w-[98vw] sm:w-[400px]" : "w-[220px]",
+ )}
+ >
+ <div
+ className="bg-blue-600 rounded border-white-300
cursor-pointer py-1 px-4
hover:bg-blue-700 transition-colors duration-200"
- onClick={() => setShowProfiling(!showProfiling)}
- >
- <ProfilingHeader />
-
- <h4 className="text-[10px] text-center italic text-white-300 py-0">
- (Click to {showProfiling ? 'hide' : 'show'})
- </h4>
- </div>
-
- {showProfiling && (
- <ProfilingModalBody />
- )}
-
- </div>
- )
-}
-
-export {
- Profiling
+ onClick={() => setShowProfiling(!showProfiling)}
+ >
+ <ProfilingHeader />
+
+ <h4 className="text-[10px] text-center italic text-white-300 py-0">
+ (Click to {showProfiling ? "hide" : "show"})
+ </h4>
+ </div>
+
+ {showProfiling && <ProfilingModalBody />}
+ </div>
+ );
};
+
+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<TData extends RowData, TValue> {
- align?: 'left' | 'center' | 'right';
- }
+declare module "@tanstack/react-table" {
+ interface ColumnMeta<TData extends RowData, TValue> {
+ 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 (
- <div className={`w-full py-1 px-1 text-white rounded ${bgClass}`}
- style={bgStyle}
- >
- <h2 className="text-xs font-semibold flex justify-between items-center">
- <span className="text-left">User Wallet:</span>
-
- {amount >= 0 ? (
- <span className='text-emerald-50'>
- {`${formatCentsToUSD(Math.abs(amount))}`}
- </span>
- ) : (
- <span className='text-rose-50'>
- {`(${formatCentsToUSD(Math.abs(amount))})`}
- </span>
- )}
-
- </h2>
- </div>
- )
+ 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 (
+ <div
+ className={`w-full py-1 px-1 text-white rounded ${bgClass}`}
+ style={bgStyle}
+ >
+ <h2 className="text-xs font-semibold flex justify-between items-center">
+ <span className="text-left">User Wallet:</span>
+
+ {amount >= 0 ? (
+ <span className="text-emerald-50">
+ {`${formatCentsToUSD(Math.abs(amount))}`}
+ </span>
+ ) : (
+ <span className="text-rose-50">
+ {`(${formatCentsToUSD(Math.abs(amount))})`}
+ </span>
+ )}
+ </h2>
+ </div>
+ );
};
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 (
- <div className="grid grid-cols-[auto_1fr] text-xs">
- <div className="label text-left">Completes:</div>
- <div className="value text-right">{survey_completes_f} surveys</div>
-
- <div className="label text-left">Avg Payouts:</div>
- <div className="value text-right">{formatCentsToUSD(min_payout)} – {formatCentsToUSD(max_payout)}</div>
-
- <div className="label text-left">Survey Adjustments:</div>
- <div className="value text-right">{survey_adjustments_f}</div>
-
- <div className="label text-left">Adjustment Amount:</div>
- <div className="value text-right">
- {adj_total >= 0 ? (
- <span className='text-emerald-300'>
- {`${formatCentsToUSD(adj_total)}`}
- </span>
- ) : (
- <span className='text-rose-300'>
- {`(${formatCentsToUSD(Math.abs(adj_total))})`}
- </span>
- )}
- </div>
-
- <div className="label text-left">User Payments:</div>
- <div className="value text-right">
- {user_payments_total >= 0 ? (
- <span className='text-emerald-300'>
- {`${formatCentsToUSD(user_payments_total)}`}
- </span>
- ) : (
- <span className='text-rose-300'>
- {`(${formatCentsToUSD(Math.abs(user_payments_total))})`}
- </span>
- )}
-
- </div>
- </div >
- )
+ 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 (
+ <div className="grid grid-cols-[auto_1fr] text-xs">
+ <div className="label text-left">Completes:</div>
+ <div className="value text-right">{survey_completes_f} surveys</div>
+
+ <div className="label text-left">Avg Payouts:</div>
+ <div className="value text-right">
+ {formatCentsToUSD(min_payout)} – {formatCentsToUSD(max_payout)}
+ </div>
+
+ <div className="label text-left">Survey Adjustments:</div>
+ <div className="value text-right">{survey_adjustments_f}</div>
+
+ <div className="label text-left">Adjustment Amount:</div>
+ <div className="value text-right">
+ {adj_total >= 0 ? (
+ <span className="text-emerald-300">
+ {`${formatCentsToUSD(adj_total)}`}
+ </span>
+ ) : (
+ <span className="text-rose-300">
+ {`(${formatCentsToUSD(Math.abs(adj_total))})`}
+ </span>
+ )}
+ </div>
+
+ <div className="label text-left">User Payments:</div>
+ <div className="value text-right">
+ {user_payments_total >= 0 ? (
+ <span className="text-emerald-300">
+ {`${formatCentsToUSD(user_payments_total)}`}
+ </span>
+ ) : (
+ <span className="text-rose-300">
+ {`(${formatCentsToUSD(Math.abs(user_payments_total))})`}
+ </span>
+ )}
+ </div>
+ </div>
+ );
};
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 (
- <div className="w-full bg-blue-600 text-white rounded text-left">
- <div className="hover:cursor-pointer hover:bg-blue-700 transition-colors duration-200"
- onClick={() => setShowTx(!showTx)}>
-
- <h2 className="text-xs font-semibold rounded flex justify-between
+ const tx_cnt = useAppSelector((state) => state.app.txTotalItems);
+ const txt_cnt_f = (tx_cnt ?? 0).toLocaleString("en-US");
+
+ const [showTx, setShowTx] = useState(false);
+
+ return (
+ <div className="w-full bg-blue-600 text-white rounded text-left">
+ <div
+ className="hover:cursor-pointer hover:bg-blue-700 transition-colors duration-200"
+ onClick={() => setShowTx(!showTx)}
+ >
+ <h2
+ className="text-xs font-semibold rounded flex justify-between
py-1 px-1"
- onClick={() => setShowTx(!showTx)}
- >
- <span>Total Transactions:</span> <span>{txt_cnt_f}</span>
- </h2>
-
- <h4 className="text-[10px] text-center italic text-blue-300 py-1"
- onClick={() => setShowTx(!showTx)}
- >
- (Click to {showTx ? 'hide' : 'show'})
- </h4>
-
- </div>
-
- {showTx && (
- <>
- <WalletTransactions />
- </>
- )}
+ onClick={() => setShowTx(!showTx)}
+ >
+ <span>Total Transactions:</span> <span>{txt_cnt_f}</span>
+ </h2>
- </div>
- )
+ <h4
+ className="text-[10px] text-center italic text-blue-300 py-1"
+ onClick={() => setShowTx(!showTx)}
+ >
+ (Click to {showTx ? "hide" : "show"})
+ </h4>
+ </div>
+
+ {showTx && (
+ <>
+ <WalletTransactions />
+ </>
+ )}
+ </div>
+ );
};
-
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<UserLedgerTransactionsResponseTransactionsInner>()
- 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<UserLedgerTransactionsResponseTransactionsInner>();
+ 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 (
+ <span className={colorClass}>
+ {`${emoji} ${formatCentsToUSD(Math.abs(val))}`}
+ </span>
+ );
+ },
+ 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 ? (
+ <span className={colorClass}>
+ {`${formatCentsToUSD(Math.abs(val))}`}
+ </span>
+ ) : (
+ <span className={colorClass}>
+ {`(${formatCentsToUSD(Math.abs(val))})`}
+ </span>
+ )}
+ </>
+ );
+ },
+ 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 (
+ <div className="w-full border-t-1 border-blue-800 p-2">
+ <table className="text-xs table-fixed border-collapse">
+ <thead className="font-bold">
+ {table.getHeaderGroups().map((headerGroup) => (
+ <tr key={headerGroup.id}>
+ {headerGroup.headers.map((header) => {
+ const align = header.column.columnDef.meta?.align || "left";
+ const alignClass =
+ align === "center"
+ ? "text-center"
+ : align === "right"
+ ? "text-right"
+ : "text-left";
return (
- <span className={colorClass}>
- {`${emoji} ${formatCentsToUSD(Math.abs(val))}`}
- </span>
- )
- },
- 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';
+ <th
+ key={header.id}
+ className={`p-0 m-0 ${alignClass}`}
+ style={{
+ width: `${header.column.getSize()}px`,
+ }}
+ >
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+ </th>
+ );
+ })}
+ </tr>
+ ))}
+ </thead>
+ <tbody>
+ {table.getRowModel().rows.map((row) => (
+ <tr key={row.id}>
+ {row.getVisibleCells().map((cell) => {
+ const align = cell.column.columnDef.meta?.align || "left";
+ const alignClass =
+ align === "center"
+ ? "text-center"
+ : align === "right"
+ ? "text-right"
+ : "text-left";
return (
- <>
- {isPositive ? (
- <span className={colorClass}>
- {`${formatCentsToUSD(Math.abs(val))}`}
- </span>
- ) : (
- <span className={colorClass}>
- {`(${formatCentsToUSD(Math.abs(val))})`}
- </span>
- )}
- </>
- )
- },
- 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 (
- <div className="w-full border-t-1 border-blue-800 p-2">
- <table className="text-xs table-fixed border-collapse">
- <thead className="font-bold">
- {table.getHeaderGroups().map((headerGroup) => (
-
- <tr key={headerGroup.id}>
- {headerGroup.headers.map((header) => {
- const align = header.column.columnDef.meta?.align || 'left';
- const alignClass = align === 'center' ? 'text-center' : align === 'right' ? 'text-right' : 'text-left';
-
- return (
- <th
- key={header.id}
- className={`p-0 m-0 ${alignClass}`}
- style={{
- width: `${header.column.getSize()}px`,
- }}
- >
- {flexRender(
- header.column.columnDef.header,
- header.getContext(),
- )}
- </th>
- )
- })}
- </tr>
- ))}
- </thead>
- <tbody>
- {table.getRowModel().rows.map((row) => (
- <tr key={row.id}>
- {row.getVisibleCells().map((cell) => {
- const align = cell.column.columnDef.meta?.align || 'left';
- const alignClass = align === 'center' ? 'text-center' :
- align === 'right' ? 'text-right' : 'text-left';
-
- return (
- <td key={cell.id}
- className={`p-0 ${alignClass}`}
- style={{
- width: `${cell.column.getSize()}px`,
- }}
- >
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
- </td>
- )
- })}
- </tr>
- ))}
- </tbody>
-
- <tfoot>
- <tr>
- <td colSpan={columns.length} className="p-2">
- <div className="flex items-center justify-between text-xs">
-
- {/* Page info */}
- <div className="text-blue-50">
- Page {table.getState().pagination.pageIndex + 1} of{' '}
- {table.getPageCount()}
- </div>
-
- {/* Navigation buttons */}
- <div className="flex gap-8 text-xs">
- <button
- onClick={() => table.previousPage()}
- disabled={!table.getCanPreviousPage()}
- className="px-3 py-1 border border-blue-200 rounded
+ <td
+ key={cell.id}
+ className={`p-0 ${alignClass}`}
+ style={{
+ width: `${cell.column.getSize()}px`,
+ }}
+ >
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ </td>
+ );
+ })}
+ </tr>
+ ))}
+ </tbody>
+
+ <tfoot>
+ <tr>
+ <td colSpan={columns.length} className="p-2">
+ <div className="flex items-center justify-between text-xs">
+ {/* Page info */}
+ <div className="text-blue-50">
+ Page {table.getState().pagination.pageIndex + 1} of{" "}
+ {table.getPageCount()}
+ </div>
+
+ {/* Navigation buttons */}
+ <div className="flex gap-8 text-xs">
+ <button
+ onClick={() => table.previousPage()}
+ disabled={!table.getCanPreviousPage()}
+ className="px-3 py-1 border border-blue-200 rounded
font-variant-emoji-text
hover:cursor-pointer hover:bg-blue-700
disabled:opacity-50 disabled:cursor-not-allowed"
- >
- {'⬅\uFE0E'}
- </button>
- <button
- onClick={() => table.nextPage()}
- disabled={!table.getCanNextPage()}
- className="px-3 py-1 border border-blue-200 rounded
+ >
+ {"⬅\uFE0E"}
+ </button>
+ <button
+ onClick={() => table.nextPage()}
+ disabled={!table.getCanNextPage()}
+ className="px-3 py-1 border border-blue-200 rounded
font-variant-emoji-text
hover:cursor-pointer hover:bg-blue-700
disabled:opacity-50 disabled:cursor-not-allowed"
- >
- {'⮕\uFE0E'}
- </button>
- </div>
-
- {/* Page size selector */}
- <select
- value={table.getState().pagination.pageSize}
- onChange={e => table.setPageSize(Number(e.target.value))}
- className="px-2 py-1 border border-blue-200 rounded
+ >
+ {"⮕\uFE0E"}
+ </button>
+ </div>
+
+ {/* Page size selector */}
+ <select
+ value={table.getState().pagination.pageSize}
+ onChange={(e) => table.setPageSize(Number(e.target.value))}
+ className="px-2 py-1 border border-blue-200 rounded
hover:cursor-pointer hover:bg-blue-700"
- >
- {[10, 20, 30, 40].map(pageSize => (
- <option key={pageSize} value={pageSize}>
- Show {pageSize}
- </option>
- ))}
- </select>
- </div>
- </td>
- </tr>
- </tfoot>
-
- </table>
- </div>
- )
+ >
+ {[10, 20, 30, 40].map((pageSize) => (
+ <option key={pageSize} value={pageSize}>
+ Show {pageSize}
+ </option>
+ ))}
+ </select>
+ </div>
+ </td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ );
};
-
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 (
- <div className="fixed top-2 left-2 p-2 rounded
+ 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 (
+ <div
+ className="fixed top-2 left-2 p-2 rounded
bg-blue-500 text-white shadow-lg z-50
font-mono
max-h-[calc(100vh-2.5rem)] overflow-y-auto
max-w-[96vw]
- ">
- <WalletHeader />
- <WalletSummary />
- <WalletTransactionsHeader />
- </div>
- )
+ "
+ >
+ <WalletHeader />
+ <WalletSummary />
+ <WalletTransactionsHeader />
+ </div>
+ );
};
-
-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<typeof buttonVariants> & {
- asChild?: boolean
+ asChild?: boolean;
}) {
- const Comp = asChild ? Slot.Root : "button"
+ const Comp = asChild ? Slot.Root : "button";
return (
<Comp
@@ -58,7 +58,7 @@ function Button({
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
- )
+ );
}
-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({
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
- )
+ );
}
-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<Api>({} as Api)
+const ConfettiContext = createContext<Api>({} as Api);
// Define component first
const ConfettiComponent = forwardRef<ConfettiRef, Props>((props, ref) => {
@@ -40,76 +40,76 @@ const ConfettiComponent = forwardRef<ConfettiRef, Props>((props, ref) => {
manualstart = false,
children,
...rest
- } = props
- const instanceRef = useRef<ConfettiInstance | null>(null)
+ } = props;
+ const instanceRef = useRef<ConfettiInstance | null>(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 (
<ConfettiContext.Provider value={api}>
<canvas ref={canvasRef} {...rest} />
{children}
</ConfettiContext.Provider>
- )
-})
+ );
+});
// 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<HTMLButtonElement>) => {
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 (
<Button onClick={handleClick} {...props}>
{children}
</Button>
- )
-}
+ );
+};
-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<SVGSVGElement> {
- 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<SVGSVGElement>(null)
- const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
+ const id = useId();
+ const containerRef = useRef<SVGSVGElement>(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 (
<svg
@@ -112,7 +112,7 @@ export function DotPattern({
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full text-neutral-400/80",
- className
+ className,
)}
{...props}
>
@@ -152,5 +152,5 @@ export function DotPattern({
/>
))}
</svg>
- )
+ );
}
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 (
<div
@@ -164,7 +164,7 @@ function FieldSeparator({
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
- className
+ className,
)}
{...props}
>
@@ -178,7 +178,7 @@ function FieldSeparator({
</span>
)}
</div>
- )
+ );
}
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 (
<ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map(
(error, index) =>
- error?.message && <li key={index}>{error.message}</li>
+ error?.message && <li key={index}>{error.message}</li>,
)}
</ul>
- )
- }, [children, errors])
+ );
+ }, [children, errors]);
if (!content) {
- return null
+ return null;
}
return (
@@ -229,7 +229,7 @@ function FieldError({
>
{content}
</div>
- )
+ );
}
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<HTMLDivElement>) => {
- 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 (
<div
@@ -97,5 +97,5 @@ export function MagicCard({
/>
<div className="relative">{children}</div>
</div>
- )
+ );
}
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 <li data-slot="pagination-item" {...props} />
+ return <li data-slot="pagination-item" {...props} />;
}
type PaginationLinkProps = {
- isActive?: boolean
+ isActive?: boolean;
} & Pick<React.ComponentProps<typeof Button>, "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({
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
- )
+ );
}
function PaginationNext({
@@ -96,7 +96,7 @@ function PaginationNext({
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
- )
+ );
}
function PaginationEllipsis({
@@ -113,7 +113,7 @@ function PaginationEllipsis({
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
- )
+ );
}
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 };