diff options
| -rw-r--r-- | src/Widget.tsx | 19 | ||||
| -rw-r--r-- | src/components/nav-main.tsx | 18 | ||||
| -rw-r--r-- | src/lib/utils.ts | 7 | ||||
| -rw-r--r-- | src/models/app.ts | 2 | ||||
| -rw-r--r-- | src/models/upkAnswerSlice.ts | 23 | ||||
| -rw-r--r-- | src/models/userMarketplaceAnswerSlice.ts (renamed from src/models/marketplaceAnswerSlice.ts) | 15 | ||||
| -rw-r--r-- | src/models/userUpkAnswerSlice.ts | 54 | ||||
| -rw-r--r-- | src/pages/Demographics.tsx | 122 | ||||
| -rw-r--r-- | src/store.ts | 9 |
9 files changed, 231 insertions, 38 deletions
diff --git a/src/Widget.tsx b/src/Widget.tsx index dc24bea..e1522c0 100644 --- a/src/Widget.tsx +++ b/src/Widget.tsx @@ -4,9 +4,19 @@ import {SiteHeader} from "@/components/site-header" import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar" import {Offerwall} from "@/pages/Offerwall.tsx" import {QuestionsPage} from "@/pages/Questions.tsx"; +import {Demographics} from "@/pages/Demographics.tsx" import {useAppDispatch, useAppSelector} from "@/hooks.ts"; -import {CashoutMethodOut, OfferwallApi, ProfilingQuestionsApi, QuestionInfo, UserWalletBalance, WalletApi} from "@/api"; +import { + CashoutMethodOut, + MarketProfileKnowledge, + OfferwallApi, + ProfilingQuestionsApi, + QuestionInfo, + UserProfileKnowledge, + UserWalletBalance, + WalletApi +} from "@/api"; import {ProfileQuestion, setQuestions} from "@/models/questionSlice.ts"; import {setBuckets} from "@/models/bucketSlice.ts"; import {setCashoutMethods} from "@/models/cashoutMethodSlice.ts"; @@ -14,6 +24,8 @@ import {setWallet} from "@/models/walletSlice.ts" import {CashoutMethodsPage} from "@/pages/CashoutMethods.tsx"; import {setUpkQuestions} from "@/models/upkQuestionSlice.ts" import {setAvailabilityCount, setOfferwallId} from "@/models/appSlice.ts" +import {setUpkAnswers} from "@/models/userUpkAnswerSlice.ts"; +import {setMarketplaceAnswers} from "@/models/userMarketplaceAnswerSlice.ts"; import './index.css'; @@ -46,8 +58,8 @@ const Widget = () => { new ProfilingQuestionsApi().userProfileProductIdUserProfileGet(app.bpid, app.bpuid, "us") .then(res => { - console.log("Marketplace Profile", res.data["user-profile"].marketplace_profile_knowledge) - console.log("UPK Profile", res.data["user-profile"].user_profile_knowledge) + dispatch(setMarketplaceAnswers(res.data["user-profile"].marketplace_profile_knowledge as MarketProfileKnowledge[])) + dispatch(setUpkAnswers(res.data["user-profile"].user_profile_knowledge as UserProfileKnowledge[])) }).catch(err => console.log(err)) new ProfilingQuestionsApi().profilingInfoProductIdProfilingInfoGet(app.bpid, "us") @@ -86,6 +98,7 @@ const Widget = () => { {app.currentPage === 'offerwall' && <Offerwall/>} {app.currentPage === 'questions' && <QuestionsPage/>} {app.currentPage === 'cashout_methods' && <CashoutMethodsPage/>} + {app.currentPage === 'demographics' && <Demographics/>} </div> </div> </div> diff --git a/src/components/nav-main.tsx b/src/components/nav-main.tsx index 1f276b1..fbd0ff8 100644 --- a/src/components/nav-main.tsx +++ b/src/components/nav-main.tsx @@ -1,6 +1,6 @@ "use client" -import {ListIcon, NotebookText, Users} from "lucide-react" +import {ListIcon, NotebookText, Users, User} from "lucide-react" import { SidebarGroup, SidebarGroupContent, @@ -13,12 +13,14 @@ import {useAppDispatch, useAppSelector} from "@/hooks.ts"; import {useSelector} from "react-redux"; import {selectQuestions} from "@/models/questionSlice.ts"; import {Badge} from "@/components/ui/badge" +import {selectUserUpkAnswers} from "@/models/userUpkAnswerSlice.ts"; export function NavMain() { const dispatch = useAppDispatch() const app = useAppSelector(state => state.app) const questions = useSelector(selectQuestions) + const upkAnswers = useSelector(selectUserUpkAnswers) return ( <SidebarGroup> @@ -56,6 +58,20 @@ export function NavMain() { </SidebarMenuButton> </SidebarMenuItem> + <SidebarMenuItem key="demographics" + onClick={() => dispatch(setPage("demographics"))} + > + <SidebarMenuButton tooltip="User Demographics"> + <User/> + <span> + Demographics <Badge + className="absolute top-2 right-2 h-5 min-w-5 rounded-full px-1 font-mono tabular-nums cursor-pointer" + variant="outline" + >{upkAnswers.length.toLocaleString()}</Badge> + </span> + </SidebarMenuButton> + </SidebarMenuItem> + <SidebarMenuItem key="community"> <SidebarMenuButton tooltip="Community"> <Users/> diff --git a/src/lib/utils.ts b/src/lib/utils.ts index b1cd120..ed75d2f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -34,3 +34,10 @@ export function formatCentsToUSD(cents: number): string { currency: 'USD', }).format(cents / 100) } + +export function titleCase(str: string): string { + return str + .split(" ") + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +}
\ No newline at end of file diff --git a/src/models/app.ts b/src/models/app.ts index 25b2412..27923db 100644 --- a/src/models/app.ts +++ b/src/models/app.ts @@ -1,4 +1,4 @@ -export type Page = 'offerwall' | 'questions' | 'cashout_methods'; +export type Page = 'offerwall' | 'questions' | 'demographics' | 'cashout_methods'; export interface App { targetId: string, diff --git a/src/models/upkAnswerSlice.ts b/src/models/upkAnswerSlice.ts deleted file mode 100644 index 7bd57f1..0000000 --- a/src/models/upkAnswerSlice.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {createSlice, PayloadAction} from '@reduxjs/toolkit' -import {UserProfileKnowledge} from "@/api"; - -const upkInitialState: UserProfileKnowledge[] = [] - -const upkAnswerSlice = createSlice({ - name: 'upkAnswers', - upkInitialState, - reducers: { - setUpkAnswers(state, action: PayloadAction<UserProfileKnowledge[]>) { - const existingIds = new Set(state.map(q => q.property_id)); - const newQuestions = action.payload.filter(q => !existingIds.has(q.property_id)); - state.push(...newQuestions); - } - } -}) - - -export const { - setUpkAnswers, -} = upkAnswerSlice.actions; - -export default upkAnswerSlice.reducer;
\ No newline at end of file diff --git a/src/models/marketplaceAnswerSlice.ts b/src/models/userMarketplaceAnswerSlice.ts index a1db7f0..7cfe95b 100644 --- a/src/models/marketplaceAnswerSlice.ts +++ b/src/models/userMarketplaceAnswerSlice.ts @@ -1,12 +1,13 @@ import {createSlice, PayloadAction} from "@reduxjs/toolkit"; import {MarketProfileKnowledge} from "@/api"; +import type {RootState} from '@/store' -const marketplaceInitialState: MarketProfileKnowledge[] = [] +const initialState: MarketProfileKnowledge[] = [] -const marketplaceAnswerSlice = createSlice({ - name: 'marketplaceAnswers', - marketplaceInitialState, +const userMarketplaceAnswerSlice = createSlice({ + name: 'userMarketplaceAnswers', + initialState, reducers: { setMarketplaceAnswers(state, action: PayloadAction<MarketProfileKnowledge[]>) { // TODO: Does this need question_id + source uniqueness? @@ -19,6 +20,8 @@ const marketplaceAnswerSlice = createSlice({ export const { setMarketplaceAnswers, -} = marketplaceAnswerSlice.actions; +} = userMarketplaceAnswerSlice.actions; -export default marketplaceAnswerSlice.reducer; +export default userMarketplaceAnswerSlice.reducer; + +export const selectUserMarketplaceAnswers = (state: RootState) => state.userMarketplaceAnswers
\ No newline at end of file diff --git a/src/models/userUpkAnswerSlice.ts b/src/models/userUpkAnswerSlice.ts new file mode 100644 index 0000000..9cb0e9b --- /dev/null +++ b/src/models/userUpkAnswerSlice.ts @@ -0,0 +1,54 @@ +import {createSlice, PayloadAction} from '@reduxjs/toolkit' +import {UserProfileKnowledge} from "@/api"; +import type {RootState} from "@/store.ts"; + +const initialState: UserProfileKnowledge[] = [] + +const userUpkAnswerSlice = createSlice({ + name: 'upkAnswers', + initialState, + reducers: { + setUpkAnswers(state, action: PayloadAction<UserProfileKnowledge[]>) { + const existingIds = new Set(state.map(q => q.property_id)); + const newQuestions = action.payload.filter(q => !existingIds.has(q.property_id)); + state.push(...newQuestions); + } + } +}) + + +export const { + setUpkAnswers, +} = userUpkAnswerSlice.actions; + +export default userUpkAnswerSlice.reducer; + +export const selectUserUpkAnswers = (state: RootState) => state.userUpkAnswers + + +// educational_attainment +export const selectUserAge = (state: RootState): number | null => { + let upk_a = state.userUpkAnswers.find(a => a.property_label === "age_in_years") + if (upk_a) { + return Number(upk_a.answer[0].value) + } else { + return null + } +} +export const selectUserZip = (state: RootState): string | null => { + let upk_a = state.userUpkAnswers.find(a => a.property_label === "home_postal_code") + if (upk_a) { + return upk_a.answer[0].value + } else { + return null + } +} + +export const selectUserGender = (state: RootState): string | null => { + let upk_a = state.userUpkAnswers.find(a => a.property_label === "gender") + if (upk_a) { + return upk_a.answer[0].label + } else { + return null + } +}
\ No newline at end of file diff --git a/src/pages/Demographics.tsx b/src/pages/Demographics.tsx new file mode 100644 index 0000000..1130ef4 --- /dev/null +++ b/src/pages/Demographics.tsx @@ -0,0 +1,122 @@ +import React from "react"; +import {useSelector} from "react-redux"; +import {selectUserAge, selectUserGender, selectUserUpkAnswers, selectUserZip} from "@/models/userUpkAnswerSlice.ts"; +import {titleCase} from "@/lib/utils.ts"; + +import {Card, CardContent, CardHeader} from "@/components/ui/card"; +import {Calendar, MapPin, User} from "lucide-react"; +import {BucketTask} from "@/api"; +import {ColumnDef, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; +import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table.tsx"; + +export const UpkGrid = () => { + + const columns: ColumnDef<BucketTask>[] = [ + { + accessorKey: "property_label", + header: "Label", + }, + { + accessorKey: "answer", + header: "Answer", + cell: ({getValue}) => getValue()[0].value ?? getValue()[0].label + }, + ] + + const data = useSelector(selectUserUpkAnswers) + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }) + + return ( + <div className="border rounded-md mt-6"> + <Table> + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => { + return ( + <TableHead key={header.id}> + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + </TableHead> + ) + })} + </TableRow> + ))} + </TableHeader> + + <TableBody> + { + table.getRowModel().rows.map((row) => ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + > + {row.getVisibleCells().map((cell) => ( + <TableCell key={cell.id}> + {flexRender(cell.column.columnDef.cell, cell.getContext())} + </TableCell> + ))} + </TableRow> + )) + } + </TableBody> + </Table> + </div> + ) +} + +export const ContactCard = () => { + + const age = useSelector(selectUserAge) + const zip = useSelector(selectUserZip) + const gender = useSelector(selectUserGender) + + return ( + <Card className="w-full max-w-sm shadow-md rounded-2xl p-4"> + <CardHeader className="flex items-center space-x-4"> + <div className="w-16 h-16 rounded-full bg-gray-200 flex items-center justify-center"> + <User className="text-gray-500"/> + </div> + </CardHeader> + + <CardContent className="space-y-2 pt-2"> + <div className="flex items-center space-x-2 text-sm text-muted-foreground"> + <User className="w-4 h-4"/> + <span>{titleCase(gender as string) ?? " - "}</span> + </div> + <div className="flex items-center space-x-2 text-sm text-muted-foreground"> + <Calendar className="w-4 h-4"/> + <span>{age ?? " - "} years old</span> + </div> + <div className="flex items-center space-x-2 text-sm text-muted-foreground"> + <MapPin className="w-4 h-4"/> + <span>{zip ?? " - "}</span> + </div> + </CardContent> + </Card> + ); +}; + + +const Demographics = () => { + + return ( + <> + <ContactCard/> + <UpkGrid /> + </> + ) +} + +export { + Demographics +}
\ No newline at end of file diff --git a/src/store.ts b/src/store.ts index 9600704..c4844b5 100644 --- a/src/store.ts +++ b/src/store.ts @@ -7,8 +7,8 @@ import answerReducers from "@/models/answerSlice.ts" import cashoutMethodReducers from "@/models/cashoutMethodSlice.ts" import walletReducers from "@/models/walletSlice.ts" import upkQuestionReducers from "@/models/upkQuestionSlice" -import upkAnswerReducers from "@/models/upkAnswerSlice" -import marketplaceReducers from "@/models/marketplaceAnswerSlice" +import userUpkAnswerReducers from "@/models/userUpkAnswerSlice.ts" +import userMarketplaceReducers from "@/models/userMarketplaceAnswerSlice.ts" export const store = configureStore({ reducer: { @@ -20,8 +20,9 @@ export const store = configureStore({ questions: questionReducers, upkQuestions: upkQuestionReducers, - upkAnswers: upkAnswerReducers, - marketplaceAnswers: marketplaceReducers, + + userUpkAnswers: userUpkAnswerReducers, + userMarketplaceAnswers: userMarketplaceReducers, // - Read Write // -- This stores user engagement (eg: answering any questions) |
