Add option to delete an answered compatibility prompt

This commit is contained in:
MartinBraquet
2025-09-22 10:09:13 +02:00
parent 3e95467819
commit 439ac0310b
5 changed files with 117 additions and 98 deletions

View File

@@ -1,22 +1,22 @@
import { RadioGroup } from '@headlessui/react'
import { UserIcon } from '@heroicons/react/solid'
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 { User } from 'common/user'
import { shortenNumber } from 'common/util/format'
import { sortBy } from 'lodash'
import { useState } from 'react'
import { Button } from 'web/components/buttons/button'
import { Col } from 'web/components/layout/col'
import { SCROLLABLE_MODAL_CLASS } from 'web/components/layout/modal'
import { Row } from 'web/components/layout/row'
import { ExpandingInput } from 'web/components/widgets/expanding-input'
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 { filterKeys } from '../questions-form'
import {Row as rowFor, run} from 'common/supabase/utils'
import {User} from 'common/user'
import {shortenNumber} from 'common/util/format'
import {sortBy} from 'lodash'
import {useState} from 'react'
import {Button} from 'web/components/buttons/button'
import {Col} from 'web/components/layout/col'
import {SCROLLABLE_MODAL_CLASS} from 'web/components/layout/modal'
import {Row} from 'web/components/layout/row'
import {ExpandingInput} from 'web/components/widgets/expanding-input'
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 {filterKeys} from '../questions-form'
import toast from "react-hot-toast";
export type CompatibilityAnswerSubmitType = Omit<
@@ -72,7 +72,25 @@ export const submitCompatibilityAnswer = async (
console.error('Failed to upsert love_compatibility_answers:', error);
toast.error('Error submitting. Try again?')
}
}
export const deleteCompatibilityAnswer = async (
id: number,
userId: string
) => {
if (!userId || !id) return
try {
await run(
db
.from('love_compatibility_answers')
.delete()
.match({id: id, creator_id: userId})
)
await track('delete compatibility question', {id});
} catch (error) {
console.error('Failed to delete prompt answer:', error);
toast.error('Error deleting. Try again?')
}
}
function getEmptyAnswer(userId: string, questionId: number) {
@@ -85,6 +103,7 @@ function getEmptyAnswer(userId: string, questionId: number) {
importance: -1,
}
}
export function AnswerCompatibilityQuestionContent(props: {
compatibilityQuestion: QuestionWithCountType
user: User
@@ -108,7 +127,7 @@ export function AnswerCompatibilityQuestionContent(props: {
} = props
const [answer, setAnswer] = useState<CompatibilityAnswerSubmitType>(
(props.answer as CompatibilityAnswerSubmitType) ??
getEmptyAnswer(user.id, compatibilityQuestion.id)
getEmptyAnswer(user.id, compatibilityQuestion.id)
)
const [loading, setLoading] = useState(false)
@@ -158,7 +177,7 @@ export function AnswerCompatibilityQuestionContent(props: {
>
{shortenedPopularity}
</Tooltip>
<UserIcon className="h-4 w-4" />
<UserIcon className="h-4 w-4"/>
</Row>
)}
</Col>
@@ -173,7 +192,7 @@ export function AnswerCompatibilityQuestionContent(props: {
<SelectAnswer
value={answer.multiple_choice}
setValue={(choice) =>
setAnswer({ ...answer, multiple_choice: choice })
setAnswer({...answer, multiple_choice: choice})
}
options={optionOrder}
/>
@@ -183,7 +202,7 @@ export function AnswerCompatibilityQuestionContent(props: {
<MultiSelectAnswers
values={answer.pref_choices ?? []}
setValue={(choice) =>
setAnswer({ ...answer, pref_choices: choice })
setAnswer({...answer, pref_choices: choice})
}
options={optionOrder}
/>
@@ -194,7 +213,7 @@ export function AnswerCompatibilityQuestionContent(props: {
currentChoice={answer.importance ?? -1}
choicesMap={IMPORTANCE_CHOICES}
setChoice={(choice: number) =>
setAnswer({ ...answer, importance: choice })
setAnswer({...answer, importance: choice})
}
indexColors={IMPORTANCE_RADIO_COLORS}
/>
@@ -208,14 +227,14 @@ export function AnswerCompatibilityQuestionContent(props: {
rows={3}
value={answer.explanation ?? ''}
onChange={(e) =>
setAnswer({ ...answer, explanation: e.target.value })
setAnswer({...answer, explanation: e.target.value})
}
/>
</Col>
</Col>
<Row className="w-full justify-between gap-4">
{noSkip ? (
<div />
<div/>
) : (
<button
disabled={loading || skipLoading}
@@ -252,15 +271,15 @@ export function AnswerCompatibilityQuestionContent(props: {
loading={loading}
onClick={() => {
setLoading(true)
submitCompatibilityAnswer(answer)
.then(() => {
if (isLastQuestion) {
onSubmit()
} else if (onNext) {
onNext()
}
})
.finally(() => setLoading(false))
submitCompatibilityAnswer(answer)
.then(() => {
if (isLastQuestion) {
onSubmit()
} else if (onNext) {
onNext()
}
})
.finally(() => setLoading(false))
}}
>
{isLastQuestion ? 'Finish' : 'Next'}
@@ -275,7 +294,7 @@ export const SelectAnswer = (props: {
setValue: (value: number) => void
options: string[]
}) => {
const { value, setValue, options } = props
const {value, setValue, options} = props
return (
<RadioGroup
className={
@@ -288,7 +307,7 @@ export const SelectAnswer = (props: {
<RadioGroup.Option
key={i}
value={i}
className={({ disabled }) =>
className={({disabled}) =>
clsx(
disabled
? 'text-ink-300 aria-checked:bg-ink-300 aria-checked:text-ink-0 cursor-not-allowed'
@@ -310,7 +329,7 @@ export const MultiSelectAnswers = (props: {
setValue: (value: number[]) => void
options: string[]
}) => {
const { values, setValue, options } = props
const {values, setValue, options} = props
return (
<Col

View File

@@ -1,52 +1,43 @@
import { PencilIcon } from '@heroicons/react/outline'
import {
getAnswerCompatibility,
getScoredAnswerCompatibility,
} from 'common/love/compatibility-score'
import { Profile } from 'common/love/profile'
import { Row as rowFor } from 'common/supabase/utils'
import { User } from 'common/user'
import { partition, sortBy, keyBy } from 'lodash'
import { useProfile } from 'web/hooks/use-profile'
import {PencilIcon, TrashIcon} from '@heroicons/react/outline'
import {getAnswerCompatibility, getScoredAnswerCompatibility,} from 'common/love/compatibility-score'
import {Profile} from 'common/love/profile'
import {Row as rowFor} from 'common/supabase/utils'
import {User} from 'common/user'
import {keyBy, partition, sortBy} from 'lodash'
import {useProfile} from 'web/hooks/use-profile'
import {
QuestionWithCountType,
useCompatibilityQuestionsWithAnswerCount,
useUserCompatibilityAnswers,
} from 'web/hooks/use-questions'
import { useEffect, useState } from 'react'
import {useEffect, useState} from 'react'
import DropdownMenu from 'web/components/comments/dropdown-menu'
import { Col } from 'web/components/layout/col'
import {
MODAL_CLASS,
Modal,
SCROLLABLE_MODAL_CLASS,
} from 'web/components/layout/modal'
import { Row } from 'web/components/layout/row'
import { Linkify } from 'web/components/widgets/linkify'
import { Pagination } from 'web/components/widgets/pagination'
import { db } from 'web/lib/supabase/db'
import { Subtitle } from '../widgets/profile-subtitle'
import { AddCompatibilityQuestionButton } from './add-compatibility-question-button'
import {Col} from 'web/components/layout/col'
import {Modal, MODAL_CLASS, SCROLLABLE_MODAL_CLASS,} from 'web/components/layout/modal'
import {Row} from 'web/components/layout/row'
import {Linkify} from 'web/components/widgets/linkify'
import {Pagination} from 'web/components/widgets/pagination'
import {db} from 'web/lib/supabase/db'
import {Subtitle} from '../widgets/profile-subtitle'
import {AddCompatibilityQuestionButton} from './add-compatibility-question-button'
import {
AnswerCompatibilityQuestionButton,
AnswerSkippedCompatibilityQuestionsButton,
} from './answer-compatibility-question-button'
import {
AnswerCompatibilityQuestionContent,
deleteCompatibilityAnswer,
IMPORTANCE_CHOICES,
IMPORTANCE_DISPLAY_COLORS,
} from './answer-compatibility-question-content'
import clsx from 'clsx'
import { shortenName } from 'web/components/widgets/user-link'
import {
PreferredList,
PreferredListNoComparison,
} from './compatibility-question-preferred-list'
import { useUser } from 'web/hooks/use-user'
import { usePersistentInMemoryState } from 'web/hooks/use-persistent-in-memory-state'
import { useIsLooking } from 'web/hooks/use-is-looking'
import { DropdownButton } from '../filters/desktop-filters'
import { buildArray } from 'common/util/array'
import {shortenName} from 'web/components/widgets/user-link'
import {PreferredList, PreferredListNoComparison,} from './compatibility-question-preferred-list'
import {useUser} from 'web/hooks/use-user'
import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state'
import {useIsLooking} from 'web/hooks/use-is-looking'
import {DropdownButton} from '../filters/desktop-filters'
import {buildArray} from 'common/util/array'
const NUM_QUESTIONS_TO_SHOW = 8
@@ -69,7 +60,7 @@ function separateQuestionsArray(
}
})
return { skippedQuestions, answeredQuestions, otherQuestions }
return {skippedQuestions, answeredQuestions, otherQuestions}
}
type CompatibilitySort =
@@ -85,12 +76,12 @@ export function CompatibilityQuestionsDisplay(props: {
fromSignup?: boolean
fromProfilePage?: Profile
}) {
const { isCurrentUser, user, fromSignup, fromProfilePage, profile } = props
const {isCurrentUser, user, fromSignup, fromProfilePage, profile} = props
const { refreshCompatibilityQuestions, compatibilityQuestions } =
const {refreshCompatibilityQuestions, compatibilityQuestions} =
useCompatibilityQuestionsWithAnswerCount()
const { refreshCompatibilityAnswers, compatibilityAnswers } =
const {refreshCompatibilityAnswers, compatibilityAnswers} =
useUserCompatibilityAnswers(user.id)
const [skippedAnswers, answers] = partition(
@@ -106,7 +97,7 @@ export function CompatibilityQuestionsDisplay(props: {
skippedAnswers.map((answer) => answer.question_id)
)
const { skippedQuestions, answeredQuestions, otherQuestions } =
const {skippedQuestions, answeredQuestions, otherQuestions} =
separateQuestionsArray(
compatibilityQuestions,
skippedAnswerQuestionIds,
@@ -126,7 +117,7 @@ export function CompatibilityQuestionsDisplay(props: {
const currentUser = useUser()
const comparedUserId = fromProfilePage?.user_id ?? currentUser?.id
const { compatibilityAnswers: comparedAnswers } =
const {compatibilityAnswers: comparedAnswers} =
useUserCompatibilityAnswers(comparedUserId)
const questionIdToComparedAnswer = keyBy(comparedAnswers, 'question_id')
@@ -206,9 +197,9 @@ export function CompatibilityQuestionsDisplay(props: {
</span>
) : (
<span className="text-ink-600 text-sm">
Answer more questions to increase your compatibility scoresor
Answer more questions to increase your compatibility scoresor{' '}
</span>
)}{' '}
)}
<AddCompatibilityQuestionButton
refreshCompatibilityAll={refreshCompatibilityAll}
/>
@@ -269,7 +260,7 @@ function CompatibilitySortWidget(props: {
fromProfilePage: Profile | undefined
className?: string
}) {
const { sort, setSort, user, fromProfilePage, className } = props
const {sort, setSort, user, fromProfilePage, className} = props
const currentUser = useUser()
const sortToDisplay = {
@@ -286,7 +277,7 @@ function CompatibilitySortWidget(props: {
'their-important',
'disagree',
(!fromProfilePage || fromProfilePage.user_id === currentUser?.id) &&
'your-unanswered'
'your-unanswered'
)
return (
@@ -301,7 +292,7 @@ function CompatibilitySortWidget(props: {
closeOnClick
buttonClass={'!text-ink-600 !hover:!text-ink-600'}
buttonContent={(open: boolean) => (
<DropdownButton content={sortToDisplay[sort]} open={open} />
<DropdownButton content={sortToDisplay[sort]} open={open}/>
)}
menuItemsClass={'bg-canvas-0'}
menuWidth="w-48"
@@ -335,8 +326,8 @@ function CompatibilityAnswerBlock(props: {
const comparedProfile = isCurrentUser
? null
: !!fromProfilePage
? fromProfilePage
: { ...currentProfile, user: currentUser }
? fromProfilePage
: {...currentProfile, user: currentUser}
if (
!question ||
@@ -393,9 +384,16 @@ function CompatibilityAnswerBlock(props: {
items={[
{
name: 'Edit',
icon: <PencilIcon className="h-5 w-5" />,
icon: <PencilIcon className="h-5 w-5"/>,
onClick: () => setEditOpen(true),
},
{
name: 'Delete',
icon: <TrashIcon className="h-5 w-5"/>,
onClick: () => {
deleteCompatibilityAnswer(answer.id, user.id).then(() => refreshCompatibilityAll())
},
},
]}
closeOnClick
menuWidth="w-40"
@@ -409,7 +407,7 @@ function CompatibilityAnswerBlock(props: {
</Row>
<Row className="px-2 -mt-4">
{answer.explanation && (
<Linkify className="" text={answer.explanation} />
<Linkify className="" text={answer.explanation}/>
)}
</Row>
{distinctPreferredAnswersText.length > 0 && (
@@ -509,6 +507,7 @@ function CompatibilityDisplay(props: {
setAnswer2(res.data[0] ?? null)
})
}
useEffect(() => {
getComparedProfileAnswer()
}, [])
@@ -527,7 +526,7 @@ function CompatibilityDisplay(props: {
const answerCompatibility = answer2
? getAnswerCompatibility(answer1, answer2)
: //getScoredAnswerCompatibility(answer1, answer2)
undefined
undefined
const user1 = profile1.user
const user2 = profile2.user
@@ -572,11 +571,11 @@ function CompatibilityDisplay(props: {
<div className="text-ink-500 text-sm">
{shortenName(user1.name)} marked this as{' '}
<span className="font-semibold">
<ImportanceDisplay importance={answer1.importance} />
<ImportanceDisplay importance={answer1.importance}/>
</span>
</div>
{!answer2 && (
<PreferredListNoComparison question={question} answer={answer1} />
<PreferredListNoComparison question={question} answer={answer1}/>
)}
{answer2 && (
<>
@@ -597,7 +596,7 @@ function CompatibilityDisplay(props: {
{isCurrentUser ? 'You' : shortenName(user2.name)} marked this
as{' '}
<span className="font-semibold">
<ImportanceDisplay importance={answer2.importance} />
<ImportanceDisplay importance={answer2.importance}/>
</span>
</div>
<PreferredList
@@ -616,7 +615,7 @@ function CompatibilityDisplay(props: {
}
function ImportanceDisplay(props: { importance: number }) {
const { importance } = props
const {importance} = props
return (
<span className={clsx('w-fit')}>
{getStringKeyFromNumValue(importance, IMPORTANCE_CHOICES)}
@@ -629,7 +628,7 @@ function ImportanceButton(props: {
onClick: () => void
className?: string
}) {
const { importance, onClick, className } = props
const {importance, onClick, className} = props
return (
<button
onClick={onClick}
@@ -641,10 +640,11 @@ function ImportanceButton(props: {
className
)}
>
<ImportanceDisplay importance={importance} />
<ImportanceDisplay importance={importance}/>
</button>
)
}
function getStringKeyFromNumValue(
value: number,
map: Record<string, number>

View File

@@ -21,7 +21,7 @@ import {api, updateProfile} from 'web/lib/api'
import React, {useState} from 'react'
import {VisibilityConfirmationModal} from './visibility-confirmation-modal'
export default function ProfileProfileHeader(props: {
export default function ProfileHeader(props: {
user: User
profile: Profile
simpleView?: boolean

View File

@@ -1,5 +1,5 @@
import {ProfileCommentSection} from 'web/components/profile-comment-section'
import ProfileProfileHeader from 'web/components/profile/profile-profile-header'
import ProfileHeader from 'web/components/profile/profile-header'
import ProfileCarousel from 'web/components/profile-carousel'
import {Col} from 'web/components/layout/col'
import {Row} from 'web/components/layout/row'
@@ -16,7 +16,7 @@ import {Content} from "web/components/widgets/editor";
import {JSONContent} from "@tiptap/core";
import React from "react";
export function ProfileProfile(props: {
export function ProfileInfo(props: {
profile: Profile
user: User
refreshProfile: () => void
@@ -63,7 +63,7 @@ export function ProfileProfile(props: {
return (
<>
<ProfileProfileHeader
<ProfileHeader
user={user}
profile={profile}
simpleView={!!fromProfilePage}

View File

@@ -15,7 +15,7 @@ import {useSaveReferral} from 'web/hooks/use-save-referral'
import {getLoveOgImageUrl} from 'common/love/og-image'
import {getProfileRow, ProfileRow} from 'common/love/profile'
import {db} from 'web/lib/supabase/db'
import {ProfileProfile} from 'web/components/profile/profile-profile'
import {ProfileInfo} from 'web/components/profile/profile-info'
import {User} from 'common/user'
import {getUserForStaticProps} from 'common/supabase/users'
import {type GetStaticProps} from 'next'
@@ -141,7 +141,7 @@ function UserPageInner(props: ActiveUserPageProps) {
{currentUser !== undefined && (
<Col className={'gap-4'}>
{profile ? (
<ProfileProfile
<ProfileInfo
key={profile.user_id}
profile={profile}
user={user}