aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax Nanis2025-06-10 02:51:37 +0700
committerMax Nanis2025-06-10 02:51:37 +0700
commite5dac8b9d61b175b09d859643a94ea91ee6ef48d (patch)
treed98ffbca9421e5218411d43d7bf5e5beea411a36
parent897e93b3a6596d25c19fe0a3579d9cd4c9ea66e7 (diff)
downloadpanel-ui-e5dac8b9d61b175b09d859643a94ea91ee6ef48d.tar.gz
panel-ui-e5dac8b9d61b175b09d859643a94ea91ee6ef48d.zip
Demographics page. userMarketplaceAnswer & userUpkAnswer redux stores.
-rw-r--r--src/Widget.tsx19
-rw-r--r--src/components/nav-main.tsx18
-rw-r--r--src/lib/utils.ts7
-rw-r--r--src/models/app.ts2
-rw-r--r--src/models/upkAnswerSlice.ts23
-rw-r--r--src/models/userMarketplaceAnswerSlice.ts (renamed from src/models/marketplaceAnswerSlice.ts)15
-rw-r--r--src/models/userUpkAnswerSlice.ts54
-rw-r--r--src/pages/Demographics.tsx122
-rw-r--r--src/store.ts9
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)