From a674d2e03de3bd048714d9c06e4bba9d9ecdb328 Mon Sep 17 00:00:00 2001 From: Max Nanis Date: Mon, 9 Jun 2025 04:44:45 +0700 Subject: Offerwall page - using datatables to show the bucket contents, setting up tabs to allow overview and detail insights of buckets, formatting of height and layout, playing with iqr, connection to conditionally eligible sidebar --- src/index.css | 3 + src/lib/snippets.tsx | 80 +++++++++++++ src/lib/utils.ts | 26 +++- src/models/questionSlice.ts | 7 +- src/pages/Offerwall.tsx | 285 +++++++++++++++++++++++++++++++++----------- 5 files changed, 332 insertions(+), 69 deletions(-) create mode 100644 src/lib/snippets.tsx (limited to 'src') diff --git a/src/index.css b/src/index.css index 9e80986..e3beb23 100644 --- a/src/index.css +++ b/src/index.css @@ -1,5 +1,8 @@ @import "tailwindcss"; @import "tw-animate-css"; +@tailwind base; +@tailwind components; +@tailwind utilities; #testD4rN { max-width: 800px; diff --git a/src/lib/snippets.tsx b/src/lib/snippets.tsx new file mode 100644 index 0000000..5e95cd2 --- /dev/null +++ b/src/lib/snippets.tsx @@ -0,0 +1,80 @@ +import React from "react"; + +type IQRData = { + min: number; + q1: number; + median: number; + q3: number; + max: number; +}; + +type IQRBoxPlotProps = { + data: IQRData; +}; + +export const IQRBoxPlot: React.FC = ({data}) => { + const {min, q1, median, q3, max} = data; + + const scale = (value: number): number => + ((value - min) / (max - min)) * 100; + + return ( +
+
Box Plot
+ + + + + + + + + + + + + +
+ ) +}; \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 8525468..b1cd120 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,5 +1,6 @@ import {type ClassValue, clsx} from "clsx" import {twMerge} from "tailwind-merge" +import React from "react"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) @@ -9,4 +10,27 @@ export function assert(condition: any, msg?: string): asserts condition { if (!condition) { throw new Error(msg); } -} \ No newline at end of file +} + +export function formatSecondsVerbose(seconds: number): string { + const mins = Math.floor(seconds / 60) + const secs = seconds % 60 + const parts = [] + if (mins > 0) parts.push(`${mins} min`) + if (secs > 0 || mins === 0) parts.push(`${secs} sec`) + return parts.join(" ") +} + +export function formatSeconds(seconds: number): string { + const mins = Math.floor(seconds / 60) + const secs = seconds % 60 + const paddedSecs = secs.toString().padStart(2, '0') + return `${mins}:${paddedSecs}` +} + +export function formatCentsToUSD(cents: number): string { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }).format(cents / 100) +} diff --git a/src/models/questionSlice.ts b/src/models/questionSlice.ts index a617234..3ef2218 100644 --- a/src/models/questionSlice.ts +++ b/src/models/questionSlice.ts @@ -15,8 +15,13 @@ const questionSlice = createSlice({ name: 'questions', initialState, reducers: { + // setQuestions(state, action: PayloadAction) { + // return action.payload; + // }, setQuestions(state, action: PayloadAction) { - return action.payload; + const existingIds = new Set(state.map(q => q.question_id)); + const newQuestions = action.payload.filter(q => !existingIds.has(q.question_id)); + state.push(...newQuestions); }, questionAdded(state, action: PayloadAction) { state.push(action.payload); diff --git a/src/pages/Offerwall.tsx b/src/pages/Offerwall.tsx index 178e762..0d0534e 100644 --- a/src/pages/Offerwall.tsx +++ b/src/pages/Offerwall.tsx @@ -1,30 +1,46 @@ -import React from 'react' -import {Separator} from "@/components/ui/separator" +import React, {useState} from 'react' +import { motion } from "framer-motion"; +import {Badge} from "@/components/ui/badge" import {Link} from '@mui/material'; -import {useSelector} from "react-redux"; - -import {Card, CardContent, CardFooter, CardHeader, CardTitle} from "@/components/ui/card.tsx"; -import {ScrollArea} from "@/components/ui/scroll-area.tsx"; -import {makeSelectQuestionsByIds, setNextQuestion, setQuestions} from "@/models/questionSlice.ts" -import {CheckIcon, MessageCircleQuestionIcon, XIcon} from "lucide-react" +import {Tabs, TabsContent} from "@/components/ui/tabs" +import {Button} from "@/components/ui/button" import { BodyOfferwallSoftpairPostProductIdOfferwall37d1da64OfferwallIdPost, + BucketTask, OfferwallApi, SoftPairBucket, UserQuestionAnswerIn } from "@/api" + +import {ColumnDef, flexRender, getCoreRowModel, useReactTable,} from "@tanstack/react-table" + +import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table" +import {useSelector} from "react-redux"; +import {Switch} from "@/components/ui/switch" +import {Card, CardContent, CardFooter} from "@/components/ui/card.tsx"; +import {makeSelectQuestionsByIds, ProfileQuestion} from "@/models/questionSlice.ts" +import {CheckIcon, MessageCircleQuestionIcon, XIcon} from "lucide-react" import {useAppDispatch, useAppSelector} from '@/hooks' import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger,} from "@/components/ui/sheet" import {ProfileQuestionFull} from "@/pages/Questions.tsx" -import {Answer, saveAnswer, selectAnswerForQuestion, submitAnswer} from "@/models/answerSlice.ts"; -import {assert} from "@/lib/utils.ts"; +import {Answer, selectAnswerForQuestion, submitAnswer} from "@/models/answerSlice.ts"; +import {assert, formatCentsToUSD, formatSeconds} from "@/lib/utils.ts"; +import {IQRBoxPlot} from "@/lib/snippets.tsx" import {setAvailabilityCount, setOfferwallId} from "@/models/appSlice.ts"; import {setBuckets} from "@/models/bucketSlice.ts"; +interface DataTableProps { + columns: ColumnDef[] + data: TData[] +} + const BucketStatus: React.FC = ({bucket}) => { switch (bucket.eligibility) { - case "eligible": - return ; + case "unconditional": + return < + CheckIcon + xlinkTitle="you are good" + />; case "conditional": return ; case "ineligible": @@ -32,6 +48,72 @@ const BucketStatus: React.FC = ({bucket}) => { } } +const ContentsGrid: React.FC = ({bucket}) => { + + const columns: ColumnDef[] = [ + { + accessorKey: "loi", + header: "Length", + cell: ({ getValue }) => formatSeconds(getValue() as number), + }, + { + accessorKey: "payout", + header: "Payout", + cell: ({ getValue}) => formatCentsToUSD(getValue() as number) + }, + ] + + const data: BucketTask[] = bucket.contents + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + + { + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + } + +
+
+ ) +} + const ConditionalQuestions: React.FC = ({bucket}) => { const dispatch = useAppDispatch() @@ -102,14 +184,34 @@ const ConditionalQuestions: React.FC = ({bucket}) => { const CallToAction: React.FC = ({bucket}) => { switch (bucket.eligibility) { - case "eligible": - return - - ; + case "unconditional": + return ( +
+ + + +
+ ) case "conditional": - return + // return + return ( +
+ +
+ ) case "ineligible": return