mirror of
https://github.com/CompassConnections/Compass.git
synced 2025-12-23 22:18:43 -05:00
Add /compatibility page to browse all the questions
This commit is contained in:
@@ -13,6 +13,7 @@ import {getCompatibleProfilesHandler} from './compatible-profiles'
|
||||
import {createComment} from './create-comment'
|
||||
import {createCompatibilityQuestion} from './create-compatibility-question'
|
||||
import {setCompatibilityAnswer} from './set-compatibility-answer'
|
||||
import {deleteCompatibilityAnswer} from './delete-compatibility-answer'
|
||||
import {createProfile} from './create-profile'
|
||||
import {createUser} from './create-user'
|
||||
import {getCompatibilityQuestions} from './get-compatibililty-questions'
|
||||
@@ -341,6 +342,7 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
|
||||
'hide-comment': hideComment,
|
||||
'create-compatibility-question': createCompatibilityQuestion,
|
||||
'set-compatibility-answer': setCompatibilityAnswer,
|
||||
'delete-compatibility-answer': deleteCompatibilityAnswer,
|
||||
'create-vote': createVote,
|
||||
'vote': vote,
|
||||
'contact': contact,
|
||||
|
||||
30
backend/api/src/delete-compatibility-answer.ts
Normal file
30
backend/api/src/delete-compatibility-answer.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import {APIHandler} from 'api/helpers/endpoint'
|
||||
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||
import {APIError} from 'common/api/utils'
|
||||
|
||||
export const deleteCompatibilityAnswer: APIHandler<'delete-compatibility-answer'> = async (
|
||||
{id}, auth) => {
|
||||
const pg = createSupabaseDirectClient()
|
||||
|
||||
// Verify user is the answer author
|
||||
const item = await pg.oneOrNone(
|
||||
`SELECT *
|
||||
FROM compatibility_answers
|
||||
WHERE id = $1
|
||||
AND creator_id = $2`,
|
||||
[id, auth.uid]
|
||||
)
|
||||
|
||||
if (!item) {
|
||||
throw new APIError(404, 'Item not found')
|
||||
}
|
||||
|
||||
// Delete the answer
|
||||
await pg.none(
|
||||
`DELETE
|
||||
FROM compatibility_answers
|
||||
WHERE id = $1
|
||||
AND creator_id = $2`,
|
||||
[id, auth.uid]
|
||||
)
|
||||
}
|
||||
@@ -295,6 +295,47 @@ export const API = (_apiTypeCheck = {
|
||||
summary: 'Remove the pinned photo from a profile',
|
||||
tag: 'Profiles',
|
||||
},
|
||||
'create-compatibility-question': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
returns: {} as any,
|
||||
props: z.object({
|
||||
question: z.string().min(1).max(MAX_COMPATIBILITY_QUESTION_LENGTH),
|
||||
options: z.record(z.string(), z.number()),
|
||||
}),
|
||||
summary: 'Create a new compatibility question with options',
|
||||
tag: 'Compatibility',
|
||||
},
|
||||
'set-compatibility-answer': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
returns: {} as Row<'compatibility_answers'>,
|
||||
props: z
|
||||
.object({
|
||||
questionId: z.number(),
|
||||
multipleChoice: z.number(),
|
||||
prefChoices: z.array(z.number()),
|
||||
importance: z.number(),
|
||||
explanation: z.string().nullable().optional(),
|
||||
})
|
||||
.strict(),
|
||||
summary: 'Submit or update a compatibility answer',
|
||||
tag: 'Compatibility',
|
||||
},
|
||||
'get-profile-answers': {
|
||||
method: 'GET',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
props: z.object({userId: z.string()}).strict(),
|
||||
returns: {} as {
|
||||
status: 'success'
|
||||
answers: Row<'compatibility_answers'>[]
|
||||
},
|
||||
summary: 'Get compatibility answers for a profile',
|
||||
tag: 'Compatibility',
|
||||
},
|
||||
'get-compatibility-questions': {
|
||||
method: 'GET',
|
||||
authed: true,
|
||||
@@ -310,6 +351,16 @@ export const API = (_apiTypeCheck = {
|
||||
summary: 'Retrieve compatibility questions and stats',
|
||||
tag: 'Compatibility',
|
||||
},
|
||||
'delete-compatibility-answer': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
props: z.object({
|
||||
id: z.number(),
|
||||
}),
|
||||
summary: 'Delete a compatibility question',
|
||||
tag: 'Compatibility',
|
||||
},
|
||||
'like-profile': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
@@ -626,47 +677,6 @@ export const API = (_apiTypeCheck = {
|
||||
// summary: 'Get reactions for a message',
|
||||
// tag: 'Messages',
|
||||
// },
|
||||
'create-compatibility-question': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
returns: {} as any,
|
||||
props: z.object({
|
||||
question: z.string().min(1).max(MAX_COMPATIBILITY_QUESTION_LENGTH),
|
||||
options: z.record(z.string(), z.number()),
|
||||
}),
|
||||
summary: 'Create a new compatibility question with options',
|
||||
tag: 'Compatibility',
|
||||
},
|
||||
'set-compatibility-answer': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
returns: {} as Row<'compatibility_answers'>,
|
||||
props: z
|
||||
.object({
|
||||
questionId: z.number(),
|
||||
multipleChoice: z.number(),
|
||||
prefChoices: z.array(z.number()),
|
||||
importance: z.number(),
|
||||
explanation: z.string().nullable().optional(),
|
||||
})
|
||||
.strict(),
|
||||
summary: 'Submit or update a compatibility answer',
|
||||
tag: 'Compatibility',
|
||||
},
|
||||
'get-profile-answers': {
|
||||
method: 'GET',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
props: z.object({userId: z.string()}).strict(),
|
||||
returns: {} as {
|
||||
status: 'success'
|
||||
answers: Row<'compatibility_answers'>[]
|
||||
},
|
||||
summary: 'Get compatibility answers for a profile',
|
||||
tag: 'Compatibility',
|
||||
},
|
||||
'create-vote': {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { User } from 'common/user'
|
||||
import { QuestionWithCountType } from 'web/hooks/use-questions'
|
||||
import { useState } from 'react'
|
||||
import { Button } from 'web/components/buttons/button'
|
||||
import { Col } from 'web/components/layout/col'
|
||||
import { MODAL_CLASS, Modal } from 'web/components/layout/modal'
|
||||
import { AnswerCompatibilityQuestionContent } from './answer-compatibility-question-content'
|
||||
import {User} from 'common/user'
|
||||
import {QuestionWithCountType} from 'web/hooks/use-questions'
|
||||
import {useState} from 'react'
|
||||
import {Button} from 'web/components/buttons/button'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {Modal, MODAL_CLASS} from 'web/components/layout/modal'
|
||||
import {AnswerCompatibilityQuestionContent} from './answer-compatibility-question-content'
|
||||
import router from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
export function AnswerCompatibilityQuestionButton(props: {
|
||||
user: User | null | undefined
|
||||
@@ -29,7 +30,7 @@ export function AnswerCompatibilityQuestionButton(props: {
|
||||
return (
|
||||
<>
|
||||
{size === 'md' ? (
|
||||
<Button onClick={() => setOpen(true)} color="gray-outline">
|
||||
<Button onClick={() => setOpen(true)} color="none" className={'px-3 py-2 rounded-md border border-primary-600 text-ink-700 hover:bg-primary-50 hover:text-ink-900'}>
|
||||
Answer Questions{' '}
|
||||
<span className="text-primary-600 ml-2">
|
||||
+{questionsToAnswer.length}
|
||||
@@ -57,12 +58,21 @@ export function AnswerCompatibilityQuestionButton(props: {
|
||||
)
|
||||
}
|
||||
|
||||
export function CompatibilityPageButton() {
|
||||
return (
|
||||
<Link
|
||||
href="/compatibility"
|
||||
className="px-3 py-2 rounded-md border border-primary-600 text-ink-700 hover:bg-primary-50 flex items-center justify-center text-center"
|
||||
>View List of Questions</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function AnswerSkippedCompatibilityQuestionsButton(props: {
|
||||
user: User | null | undefined
|
||||
skippedQuestions: QuestionWithCountType[]
|
||||
refreshCompatibilityAll: () => void
|
||||
}) {
|
||||
const { user, skippedQuestions, refreshCompatibilityAll } = props
|
||||
const {user, skippedQuestions, refreshCompatibilityAll} = props
|
||||
const [open, setOpen] = useState(false)
|
||||
if (!user) return null
|
||||
return (
|
||||
@@ -92,7 +102,7 @@ function AnswerCompatibilityQuestionModal(props: {
|
||||
refreshCompatibilityAll: () => void
|
||||
onClose?: () => void
|
||||
}) {
|
||||
const { open, setOpen, user, otherQuestions, refreshCompatibilityAll, onClose } = props
|
||||
const {open, setOpen, user, otherQuestions, refreshCompatibilityAll, onClose} = props
|
||||
const [questionIndex, setQuestionIndex] = useState(0)
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {RadioGroup} from '@headlessui/react'
|
||||
import {UserIcon} from '@heroicons/react/solid'
|
||||
import clsx from 'clsx'
|
||||
import {Row as rowFor, run} from 'common/supabase/utils'
|
||||
import {Row as rowFor} from 'common/supabase/utils'
|
||||
import {User} from 'common/user'
|
||||
import {shortenNumber} from 'common/util/format'
|
||||
import {sortBy} from 'lodash'
|
||||
@@ -15,10 +15,9 @@ import {RadioToggleGroup} from 'web/components/widgets/radio-toggle-group'
|
||||
import {Tooltip} from 'web/components/widgets/tooltip'
|
||||
import {QuestionWithCountType} from 'web/hooks/use-questions'
|
||||
import {track} from 'web/lib/service/analytics'
|
||||
import {db} from 'web/lib/supabase/db'
|
||||
import {api} from 'web/lib/api'
|
||||
import {filterKeys} from '../questions-form'
|
||||
import toast from "react-hot-toast";
|
||||
import toast from "react-hot-toast"
|
||||
|
||||
export type CompatibilityAnswerSubmitType = Omit<
|
||||
rowFor<'compatibility_answers'>,
|
||||
@@ -70,9 +69,9 @@ export const submitCompatibilityAnswer = async (
|
||||
// Track only if upsert succeeds
|
||||
track('answer compatibility question', {
|
||||
...newAnswer,
|
||||
});
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to set compatibility answer:', error);
|
||||
console.error('Failed to set compatibility answer:', error)
|
||||
toast.error('Error submitting. Try again?')
|
||||
}
|
||||
}
|
||||
@@ -83,20 +82,15 @@ export const deleteCompatibilityAnswer = async (
|
||||
) => {
|
||||
if (!userId || !id) return
|
||||
try {
|
||||
await run(
|
||||
db
|
||||
.from('compatibility_answers')
|
||||
.delete()
|
||||
.match({id: id, creator_id: userId})
|
||||
)
|
||||
await track('delete compatibility question', {id});
|
||||
await api('delete-compatibility-answer', {id})
|
||||
await track('delete compatibility question', {id})
|
||||
} catch (error) {
|
||||
console.error('Failed to delete prompt answer:', error);
|
||||
console.error('Failed to delete prompt answer:', error)
|
||||
toast.error('Error deleting. Try again?')
|
||||
}
|
||||
}
|
||||
|
||||
function getEmptyAnswer(userId: string, questionId: number) {
|
||||
export function getEmptyAnswer(userId: string, questionId: number) {
|
||||
return {
|
||||
creator_id: userId,
|
||||
explanation: null,
|
||||
@@ -112,7 +106,7 @@ export function AnswerCompatibilityQuestionContent(props: {
|
||||
user: User
|
||||
index?: number
|
||||
total?: number
|
||||
answer?: rowFor<'compatibility_answers'> | null
|
||||
answer?: CompatibilityAnswerSubmitType | null
|
||||
onSubmit: () => void
|
||||
onNext?: () => void
|
||||
isLastQuestion: boolean
|
||||
@@ -160,11 +154,11 @@ export function AnswerCompatibilityQuestionContent(props: {
|
||||
return (
|
||||
<Col className="h-full w-full gap-4">
|
||||
<Col className="gap-1">
|
||||
{compatibilityQuestion.importance_score > 0 && <Row className="text-blue-400 -mt-4 w-full justify-start text-sm">
|
||||
<span>
|
||||
Massive upgrade coming soon! More prompts, better predictive power, filtered by category, etc.
|
||||
</span>
|
||||
</Row>}
|
||||
{/*{compatibilityQuestion.importance_score > 0 && <Row className="text-blue-400 -mt-4 w-full justify-start text-sm">*/}
|
||||
{/* <span>*/}
|
||||
{/* Massive upgrade coming soon! More prompts, better predictive power, filtered by category, etc.*/}
|
||||
{/* </span>*/}
|
||||
{/*</Row>}*/}
|
||||
{index !== null &&
|
||||
index !== undefined &&
|
||||
total !== null &&
|
||||
|
||||
@@ -22,11 +22,11 @@ import {Subtitle} from '../widgets/profile-subtitle'
|
||||
import {AddCompatibilityQuestionButton} from './add-compatibility-question-button'
|
||||
import {
|
||||
AnswerCompatibilityQuestionButton,
|
||||
AnswerSkippedCompatibilityQuestionsButton,
|
||||
AnswerSkippedCompatibilityQuestionsButton, CompatibilityPageButton,
|
||||
} from './answer-compatibility-question-button'
|
||||
import {
|
||||
AnswerCompatibilityQuestionContent,
|
||||
deleteCompatibilityAnswer,
|
||||
AnswerCompatibilityQuestionContent, CompatibilityAnswerSubmitType,
|
||||
deleteCompatibilityAnswer, getEmptyAnswer,
|
||||
IMPORTANCE_CHOICES,
|
||||
IMPORTANCE_DISPLAY_COLORS,
|
||||
} from './answer-compatibility-question-content'
|
||||
@@ -41,7 +41,7 @@ import {buildArray} from 'common/util/array'
|
||||
|
||||
const NUM_QUESTIONS_TO_SHOW = 8
|
||||
|
||||
function separateQuestionsArray(
|
||||
export function separateQuestionsArray(
|
||||
questions: QuestionWithCountType[],
|
||||
skippedAnswerQuestionIds: Set<number>,
|
||||
answeredQuestionIds: Set<number>
|
||||
@@ -199,7 +199,7 @@ export function CompatibilityQuestionsDisplay(props: {
|
||||
<span className="text-ink-600 text-sm">
|
||||
Answer more questions to increase your compatibility scores—or{' '}
|
||||
</span>
|
||||
)}
|
||||
)}
|
||||
<AddCompatibilityQuestionButton
|
||||
refreshCompatibilityAll={refreshCompatibilityAll}
|
||||
/>
|
||||
@@ -225,12 +225,15 @@ export function CompatibilityQuestionsDisplay(props: {
|
||||
</>
|
||||
)}
|
||||
{otherQuestions.length >= 1 && isCurrentUser && !fromProfilePage && (
|
||||
<AnswerCompatibilityQuestionButton
|
||||
user={user}
|
||||
otherQuestions={otherQuestions}
|
||||
refreshCompatibilityAll={refreshCompatibilityAll}
|
||||
fromSignup={fromSignup}
|
||||
/>
|
||||
<Row className={'w-full justify-center gap-8'}>
|
||||
<AnswerCompatibilityQuestionButton
|
||||
user={user}
|
||||
otherQuestions={otherQuestions}
|
||||
refreshCompatibilityAll={refreshCompatibilityAll}
|
||||
fromSignup={fromSignup}
|
||||
/>
|
||||
<CompatibilityPageButton/>
|
||||
</Row>
|
||||
)}
|
||||
{skippedQuestions.length > 0 && isCurrentUser && (
|
||||
<Row className="w-full justify-end">
|
||||
@@ -300,12 +303,13 @@ function CompatibilitySortWidget(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function CompatibilityAnswerBlock(props: {
|
||||
answer: rowFor<'compatibility_answers'>
|
||||
export function CompatibilityAnswerBlock(props: {
|
||||
answer?: rowFor<'compatibility_answers'>
|
||||
yourQuestions: QuestionWithCountType[]
|
||||
question?: QuestionWithCountType
|
||||
user: User
|
||||
isCurrentUser: boolean
|
||||
profile: Profile
|
||||
profile?: Profile
|
||||
refreshCompatibilityAll: () => void
|
||||
fromProfilePage?: Profile
|
||||
}) {
|
||||
@@ -318,11 +322,17 @@ function CompatibilityAnswerBlock(props: {
|
||||
refreshCompatibilityAll,
|
||||
fromProfilePage,
|
||||
} = props
|
||||
const question = yourQuestions.find((q) => q.id === answer.question_id)
|
||||
const question = props.question || yourQuestions.find((q) => q.id === answer?.question_id)
|
||||
const [editOpen, setEditOpen] = useState<boolean>(false)
|
||||
const currentUser = useUser()
|
||||
const currentProfile = useProfile()
|
||||
|
||||
const [newAnswer, setNewAnswer] = useState<CompatibilityAnswerSubmitType | undefined>(props.answer)
|
||||
|
||||
useEffect(() => {
|
||||
setNewAnswer(props.answer)
|
||||
}, [props.answer]);
|
||||
|
||||
const comparedProfile = isCurrentUser
|
||||
? null
|
||||
: !!fromProfilePage
|
||||
@@ -332,26 +342,27 @@ function CompatibilityAnswerBlock(props: {
|
||||
if (
|
||||
!question ||
|
||||
!question.multiple_choice_options ||
|
||||
answer.multiple_choice == null
|
||||
answer && answer?.multiple_choice == null
|
||||
)
|
||||
return null
|
||||
|
||||
const answerText = getStringKeyFromNumValue(
|
||||
const answerText = answer ? getStringKeyFromNumValue(
|
||||
answer.multiple_choice,
|
||||
question.multiple_choice_options as Record<string, number>
|
||||
)
|
||||
const preferredAnswersText = answer.pref_choices.map((choice) =>
|
||||
) : null
|
||||
const preferredAnswersText = answer ? answer.pref_choices.map((choice) =>
|
||||
getStringKeyFromNumValue(
|
||||
choice,
|
||||
question.multiple_choice_options as Record<string, number>
|
||||
)
|
||||
)
|
||||
) : []
|
||||
const distinctPreferredAnswersText = preferredAnswersText.filter(
|
||||
(text) => text !== answerText
|
||||
)
|
||||
const preferredDoesNotIncludeAnswerText =
|
||||
!preferredAnswersText.includes(answerText)
|
||||
answerText && !preferredAnswersText.includes(answerText)
|
||||
|
||||
const isAnswered = answer && answer.multiple_choice > -1
|
||||
return (
|
||||
<Col
|
||||
className={
|
||||
@@ -361,7 +372,7 @@ function CompatibilityAnswerBlock(props: {
|
||||
<Row className="text-ink-800 justify-between gap-1 font-semibold">
|
||||
{question.question}
|
||||
<Row className="gap-4 font-normal">
|
||||
{comparedProfile && (
|
||||
{comparedProfile && isAnswered && (
|
||||
<div className="hidden sm:block">
|
||||
<CompatibilityDisplay
|
||||
question={question}
|
||||
@@ -373,7 +384,7 @@ function CompatibilityAnswerBlock(props: {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isCurrentUser && (
|
||||
{isCurrentUser && isAnswered && (
|
||||
<>
|
||||
<ImportanceButton
|
||||
className="hidden sm:block"
|
||||
@@ -402,11 +413,11 @@ function CompatibilityAnswerBlock(props: {
|
||||
)}
|
||||
</Row>
|
||||
</Row>
|
||||
<Row className="bg-canvas-100 w-fit gap-1 rounded px-2 py-1 text-sm">
|
||||
{answerText && <Row className="bg-canvas-100 w-fit gap-1 rounded px-2 py-1 text-sm">
|
||||
{answerText}
|
||||
</Row>
|
||||
</Row>}
|
||||
<Row className="px-2 -mt-4">
|
||||
{answer.explanation && (
|
||||
{answer?.explanation && (
|
||||
<Linkify className="" text={answer.explanation}/>
|
||||
)}
|
||||
</Row>
|
||||
@@ -429,9 +440,29 @@ function CompatibilityAnswerBlock(props: {
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
{!isAnswered && (
|
||||
<Row className="flex-wrap gap-2 mt-0">
|
||||
{sortBy(
|
||||
Object.entries(question.multiple_choice_options),
|
||||
1
|
||||
).map(([label]) => label).map((label, i) => (
|
||||
<button
|
||||
onClick={() => {
|
||||
const _answer = getEmptyAnswer(user.id, question.id)
|
||||
_answer.multiple_choice = i
|
||||
setNewAnswer(_answer)
|
||||
setEditOpen(true)
|
||||
}}
|
||||
className="bg-canvas-100 hover:bg-canvas-200 w-fit gap-1 rounded px-2 py-1 text-sm"
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
<Col>
|
||||
|
||||
{comparedProfile && (
|
||||
{comparedProfile && isAnswered && (
|
||||
<Row className="w-full justify-end sm:hidden">
|
||||
<CompatibilityDisplay
|
||||
question={question}
|
||||
@@ -443,7 +474,7 @@ function CompatibilityAnswerBlock(props: {
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
{isCurrentUser && (
|
||||
{isCurrentUser && isAnswered && (
|
||||
<Row className="w-full justify-end sm:hidden">
|
||||
<ImportanceButton
|
||||
importance={answer.importance}
|
||||
@@ -451,13 +482,14 @@ function CompatibilityAnswerBlock(props: {
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
{/*{question.importance_score == 0 && <div className="text-ink-500 text-sm">Core Question</div>}*/}
|
||||
</Col>
|
||||
<Modal open={editOpen} setOpen={setEditOpen}>
|
||||
<Col className={MODAL_CLASS}>
|
||||
<AnswerCompatibilityQuestionContent
|
||||
key={`edit answer.id`}
|
||||
compatibilityQuestion={question}
|
||||
answer={answer}
|
||||
answer={newAnswer}
|
||||
user={user}
|
||||
onSubmit={() => {
|
||||
setEditOpen(false)
|
||||
@@ -474,7 +506,7 @@ function CompatibilityAnswerBlock(props: {
|
||||
|
||||
function CompatibilityDisplay(props: {
|
||||
question: QuestionWithCountType
|
||||
profile1: Profile
|
||||
profile1?: Profile
|
||||
profile2: Profile
|
||||
answer1: rowFor<'compatibility_answers'>
|
||||
currentUserIsComparedProfile: boolean
|
||||
@@ -514,7 +546,7 @@ function CompatibilityDisplay(props: {
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
if (profile1.id === profile2.id) return null
|
||||
if (!profile1 || profile1.id === profile2.id) return null
|
||||
|
||||
const showCreateAnswer =
|
||||
(!answer2 || answer2.importance == -1) &&
|
||||
|
||||
@@ -81,7 +81,7 @@ export const Button = forwardRef(function Button(
|
||||
props: {
|
||||
className?: string
|
||||
size?: SizeType
|
||||
color?: ColorType
|
||||
color?: ColorType | null
|
||||
type?: 'button' | 'reset' | 'submit'
|
||||
loading?: boolean
|
||||
} & JSX.IntrinsicElements['button'],
|
||||
@@ -101,7 +101,7 @@ export const Button = forwardRef(function Button(
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
className={clsx(buttonClass(size, color), className)}
|
||||
className={clsx(color && buttonClass(size, color), className)}
|
||||
disabled={disabled || loading}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
|
||||
@@ -166,7 +166,11 @@ export function ControlledTabs(props: TabProps & { activeIndex: number }) {
|
||||
<Row className="justify-center">{tab.stackedTabIcon}</Row>
|
||||
)}
|
||||
<Row className={'items-center'}>
|
||||
{tab.title}
|
||||
<Col>
|
||||
{tab.title.split('\n').map((line, _i) => (
|
||||
<Row className={'items-center justify-center'}>{line}</Row>
|
||||
))}
|
||||
</Col>
|
||||
{tab.inlineTabIcon}
|
||||
</Row>
|
||||
</Tooltip>
|
||||
@@ -212,6 +216,7 @@ export function UncontrolledTabs(props: TabProps & { defaultIndex?: number }) {
|
||||
setActiveIndex(i)
|
||||
onClick?.(titleOrQueryTitle, i)
|
||||
}}
|
||||
labelsParentClassName={'gap-0 xs:gap-4'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
187
web/pages/compatibility.tsx
Normal file
187
web/pages/compatibility.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import {useUser} from 'web/hooks/use-user'
|
||||
import {useCompatibilityQuestionsWithAnswerCount, useUserCompatibilityAnswers} from 'web/hooks/use-questions'
|
||||
import {useEffect, useMemo, useState} from 'react'
|
||||
import {Row} from 'common/supabase/utils'
|
||||
import {Question} from 'web/lib/supabase/questions'
|
||||
import {Col} from 'web/components/layout/col'
|
||||
import {Title} from 'web/components/widgets/title'
|
||||
import {PageBase} from "web/components/page-base";
|
||||
import {UncontrolledTabs} from "web/components/layout/tabs";
|
||||
import {CompatibilityAnswerBlock} from "web/components/answers/compatibility-questions-display";
|
||||
import {User} from "common/user";
|
||||
import {CompassLoadingIndicator} from "web/components/widgets/loading-indicator";
|
||||
import {useIsMobile} from "web/hooks/use-is-mobile";
|
||||
|
||||
type QuestionWithAnswer = Question & {
|
||||
answer?: Row<'compatibility_answers'>
|
||||
answer_count: number
|
||||
score: number
|
||||
}
|
||||
|
||||
export default function CompatibilityPage() {
|
||||
const user = useUser()
|
||||
const isMobile = useIsMobile()
|
||||
const sep = isMobile ? '\n' : ''
|
||||
const {compatibilityAnswers, refreshCompatibilityAnswers} =
|
||||
useUserCompatibilityAnswers(user?.id)
|
||||
const {compatibilityQuestions, refreshCompatibilityQuestions} =
|
||||
useCompatibilityQuestionsWithAnswerCount()
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const questionsWithAnswers = useMemo(() => {
|
||||
if (!compatibilityQuestions) return []
|
||||
|
||||
const answerMap = new Map(
|
||||
compatibilityAnswers?.map((a) => [a.question_id, a]) ?? []
|
||||
)
|
||||
|
||||
return compatibilityQuestions.map((q) => ({
|
||||
...q,
|
||||
answer: answerMap.get(q.id),
|
||||
})).sort(
|
||||
(a, b) => a.importance_score - b.importance_score
|
||||
) as QuestionWithAnswer[]
|
||||
}, [compatibilityQuestions, compatibilityAnswers])
|
||||
|
||||
const {answered, notAnswered, skipped} = useMemo(() => {
|
||||
const answered: QuestionWithAnswer[] = []
|
||||
const notAnswered: QuestionWithAnswer[] = []
|
||||
const skipped: QuestionWithAnswer[] = []
|
||||
|
||||
questionsWithAnswers.forEach((q) => {
|
||||
if (q.answer) {
|
||||
if (q.answer.multiple_choice === -1) {
|
||||
skipped.push(q)
|
||||
} else {
|
||||
answered.push(q)
|
||||
}
|
||||
} else {
|
||||
notAnswered.push(q)
|
||||
}
|
||||
})
|
||||
|
||||
return {answered, notAnswered, skipped}
|
||||
}, [questionsWithAnswers])
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.id) {
|
||||
Promise.all([
|
||||
refreshCompatibilityAnswers(),
|
||||
refreshCompatibilityQuestions(),
|
||||
]).finally(() => setIsLoading(false))
|
||||
}
|
||||
}, [user?.id])
|
||||
|
||||
const refreshCompatibilityAll = () => {
|
||||
refreshCompatibilityAnswers()
|
||||
refreshCompatibilityQuestions()
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<PageBase trackPageView={'compatibility'}>
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<div className="text-xl">Please sign in to view your compatibility questions</div>
|
||||
</div>
|
||||
</PageBase>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageBase trackPageView={'compatibility'}>
|
||||
<Col className="w-full p-4">
|
||||
<Title className="mb-4">Your Compatibility Questions</Title>
|
||||
<UncontrolledTabs
|
||||
trackingName={'compatibility page'}
|
||||
tabs={[
|
||||
{
|
||||
title: `Answered ${sep}(${answered.length})`,
|
||||
content: (
|
||||
<QuestionList
|
||||
questions={answered}
|
||||
status="answered"
|
||||
isLoading={isLoading}
|
||||
user={user}
|
||||
refreshCompatibilityAll={refreshCompatibilityAll}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: `To Answer ${sep}(${notAnswered.length})`,
|
||||
content: (
|
||||
<QuestionList
|
||||
questions={notAnswered}
|
||||
status="not-answered"
|
||||
isLoading={isLoading}
|
||||
user={user}
|
||||
refreshCompatibilityAll={refreshCompatibilityAll}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: `Skipped ${sep}(${skipped.length})`,
|
||||
content: (
|
||||
<QuestionList
|
||||
questions={skipped}
|
||||
status="skipped"
|
||||
isLoading={isLoading}
|
||||
user={user}
|
||||
refreshCompatibilityAll={refreshCompatibilityAll}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
</PageBase>
|
||||
)
|
||||
}
|
||||
|
||||
function QuestionList({
|
||||
questions,
|
||||
status,
|
||||
isLoading,
|
||||
user,
|
||||
refreshCompatibilityAll,
|
||||
}: {
|
||||
questions: QuestionWithAnswer[]
|
||||
status: 'answered' | 'not-answered' | 'skipped'
|
||||
isLoading: boolean
|
||||
user: User
|
||||
refreshCompatibilityAll: () => void
|
||||
}) {
|
||||
if (isLoading) {
|
||||
return <CompassLoadingIndicator/>
|
||||
}
|
||||
|
||||
if (questions.length === 0) {
|
||||
return (
|
||||
<div className="text-ink-500 p-4">
|
||||
{status === 'answered' && 'You haven\'t answered any questions yet.'}
|
||||
{status === 'not-answered' && 'All questions have been answered!'}
|
||||
{status === 'skipped' && 'You haven\'t skipped any questions.'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-2">
|
||||
{questions.map((q) => (
|
||||
<div
|
||||
key={q.id}
|
||||
className="bg-canvas-0 border-canvas-100 rounded-lg border px-2 pt-2 shadow-sm transition-colors"
|
||||
>
|
||||
<CompatibilityAnswerBlock
|
||||
key={q.answer?.question_id}
|
||||
question={q}
|
||||
answer={q.answer}
|
||||
yourQuestions={questions}
|
||||
user={user}
|
||||
isCurrentUser={true}
|
||||
refreshCompatibilityAll={refreshCompatibilityAll}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user