Add pinned compatibility questions feature with backend support and UI integration

This commit is contained in:
MartinBraquet
2026-03-19 17:00:34 +01:00
parent 891b91d0ba
commit bdafa43472
17 changed files with 383 additions and 82 deletions

View File

@@ -11,6 +11,7 @@ import {getHiddenProfiles} from 'api/get-hidden-profiles'
import {getLastMessages} from 'api/get-last-messages'
import {getMessagesCountEndpoint} from 'api/get-messages-count'
import {getOptions} from 'api/get-options'
import {getPinnedCompatibilityQuestions} from 'api/get-pinned-compatibility-questions'
import {getChannelMessagesEndpoint} from 'api/get-private-messages'
import {getUser} from 'api/get-user'
import {hideProfile} from 'api/hide-profile'
@@ -20,6 +21,7 @@ import {saveSubscriptionMobile} from 'api/save-subscription-mobile'
import {sendSearchNotifications} from 'api/send-search-notifications'
import {localSendTestEmail} from 'api/test'
import {unhideProfile} from 'api/unhide-profile'
import {updateCompatibilityQuestionPin} from 'api/update-compatibility-question-pin'
import {updateConnectionInterests} from 'api/update-connection-interests'
import {updateOptions} from 'api/update-options'
import {vote} from 'api/vote'
@@ -406,7 +408,7 @@ Most endpoints require a valid Firebase JWT token. This gives you access to your
To obtain a token:
**In your app or browser console while logged in (JavaScript/TypeScript):**
**In your browser console while logged in (CTRL+SHIFT+C, then select the Console tab):**
\`\`\`js
const db = await new Promise((res, rej) => {
const req = indexedDB.open('firebaseLocalStorageDb')
@@ -633,6 +635,8 @@ const handlers: {[k in APIPath]: APIHandler<k>} = {
'update-user-locale': updateUserLocale,
'update-private-user-message-channel': updatePrivateUserMessageChannel,
'update-profile': updateProfileEndpoint,
'update-compatibility-question-pin': updateCompatibilityQuestionPin,
'get-pinned-compatibility-questions': getPinnedCompatibilityQuestions,
'get-connection-interests': getConnectionInterestsEndpoint,
'update-connection-interest': updateConnectionInterests,
'user/by-id/:id': getUser,

View File

@@ -0,0 +1,22 @@
import type {APIHandler} from 'api/helpers/endpoint'
import {Row} from 'common/supabase/utils'
import {createSupabaseDirectClient} from 'shared/supabase/init'
export async function getPinnedQuestionIds(userId: string) {
const pg = createSupabaseDirectClient()
const rows = await pg.manyOrNone<Row<'compatibility_prompts_pinned'>>(
`select * from compatibility_prompts_pinned
where user_id = $1
order by created_time desc`,
[userId],
)
// newest-first in table; return in that order
return rows.map((r) => r.question_id)
}
export const getPinnedCompatibilityQuestions: APIHandler<
'get-pinned-compatibility-questions'
> = async (_props, auth) => {
const pinnedQuestionIds = await getPinnedQuestionIds(auth.uid)
return {status: 'success', pinnedQuestionIds}
}

View File

@@ -0,0 +1,28 @@
import {getPinnedQuestionIds} from 'api/get-pinned-compatibility-questions'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {type APIHandler} from './helpers/endpoint'
export const updateCompatibilityQuestionPin: APIHandler<
'update-compatibility-question-pin'
> = async ({questionId, pinned}, auth) => {
const pg = createSupabaseDirectClient()
if (pinned) {
await pg.none(
`insert into compatibility_prompts_pinned (user_id, question_id)
values ($1, $2)
on conflict (user_id, question_id) do nothing`,
[auth.uid, questionId],
)
} else {
await pg.none(
`delete from compatibility_prompts_pinned
where user_id = $1 and question_id = $2`,
[auth.uid, questionId],
)
}
const pinnedQuestionIds = await getPinnedQuestionIds(auth.uid)
return {status: 'success', pinnedQuestionIds}
}

View File

@@ -53,4 +53,5 @@ BEGIN;
\i backend/supabase/migrations/20260224_add_connection_preferences.sql
\i backend/supabase/migrations/20260225_add_keywords_to_profiles.sql
\i backend/supabase/migrations/20260308_add_importance_counts.sql
\i backend/supabase/migrations/20260319_add_compatibility_prompts_pinned.sql
COMMIT;

View File

@@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS compatibility_prompts_pinned
(
id BIGINT GENERATED ALWAYS AS IDENTITY NOT NULL,
created_time TIMESTAMPTZ NOT NULL DEFAULT now(),
user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
question_id BIGINT NOT NULL REFERENCES compatibility_prompts (id) ON DELETE CASCADE,
PRIMARY KEY (id),
UNIQUE (user_id, question_id)
);
ALTER TABLE compatibility_prompts_pinned
ENABLE ROW LEVEL SECURITY;
CREATE INDEX IF NOT EXISTS idx_cpp_user_created_time
ON compatibility_prompts_pinned (user_id, created_time DESC);
CREATE INDEX IF NOT EXISTS idx_cpp_user_question
ON compatibility_prompts_pinned (user_id, question_id);

View File

@@ -87,6 +87,12 @@
"answers.next": "Weiter",
"answers.opinion.edit": "Bearbeiten",
"answers.opinion.fill": "Meinungsskala ausfüllen",
"answers.pinned.pin_tooltip": "Diese Frage anpinnen, um sie zuerst auf Profilen zu sehen",
"answers.pinned.pin_aria": "Frage anpinnen",
"answers.pinned.title": "Angepinnte Fragen, die Sie zuerst sehen möchten",
"answers.pinned.unpin_tooltip": "Diese Frage losen",
"answers.pinned.unpin_aria": "Frage losen",
"answers.pinned.error": "Fehler beim Aktualisieren der Einstellung",
"answers.preferred.user_answer": "Die Antwort von {name}",
"answers.preferred.your_answer": "Ihre Antwort",
"answers.sort.important_to_them": "Wichtig für {name}",

View File

@@ -87,6 +87,12 @@
"answers.next": "Suivant",
"answers.opinion.edit": "Modifier",
"answers.opinion.fill": "Remplir l'échelle d'opinion",
"answers.pinned.pin_tooltip": "Épingler cette question pour voir la réponse des autres en premier sur leur profil",
"answers.pinned.pin_aria": "Épingler la question",
"answers.pinned.title": "Questions que vous avez épinglées",
"answers.pinned.unpin_tooltip": "Désépingler cette question",
"answers.pinned.unpin_aria": "Désépingler la question",
"answers.pinned.error": "Erreur lors de la mise à jour de la préférence",
"answers.preferred.user_answer": "La réponse de {name}",
"answers.preferred.your_answer": "Votre réponse",
"answers.sort.important_to_them": "Important pour {name}",

View File

@@ -554,6 +554,35 @@ export const API = (_apiTypeCheck = {
summary: 'Submit or update a compatibility answer',
tag: 'Compatibility',
},
'update-compatibility-question-pin': {
method: 'POST',
authed: true,
rateLimited: true,
props: z
.object({
questionId: z.number(),
pinned: z.boolean(),
})
.strict(),
returns: {} as {
status: 'success'
pinnedQuestionIds: number[]
},
summary: 'Pin or unpin a compatibility question for your profile views',
tag: 'Compatibility',
},
'get-pinned-compatibility-questions': {
method: 'GET',
authed: true,
rateLimited: false,
props: z.object({}).strict(),
returns: {} as {
status: 'success'
pinnedQuestionIds: number[]
},
summary: 'Get pinned compatibility question ids for current user',
tag: 'Compatibility',
},
'get-profile-answers': {
method: 'GET',
authed: true,

View File

@@ -224,6 +224,42 @@ export type Database = {
},
]
}
compatibility_prompts_pinned: {
Row: {
created_time: string
id: number
question_id: number
user_id: string
}
Insert: {
created_time?: string
id?: never
question_id: number
user_id: string
}
Update: {
created_time?: string
id?: never
question_id?: number
user_id?: string
}
Relationships: [
{
foreignKeyName: 'compatibility_prompts_pinned_question_id_fkey'
columns: ['question_id']
isOneToOne: false
referencedRelation: 'compatibility_prompts'
referencedColumns: ['id']
},
{
foreignKeyName: 'compatibility_prompts_pinned_user_id_fkey'
columns: ['user_id']
isOneToOne: false
referencedRelation: 'users'
referencedColumns: ['id']
},
]
}
compatibility_prompts_translations: {
Row: {
locale: string

View File

@@ -64,7 +64,7 @@ function AddCompatibilityQuestionModal(props: {
<CreateCompatibilityModalContent afterAddQuestion={afterAddQuestion} setOpen={setOpen} />
) : (
<AnswerCompatibilityQuestionContent
compatibilityQuestion={dbQuestion as QuestionWithStats}
question={dbQuestion as QuestionWithStats}
user={user}
onSubmit={() => {
// setOpen(false)

View File

@@ -221,7 +221,7 @@ function AnswerCompatibilityQuestionModal(props: {
key={sortedQuestions[questionIndex].id}
index={questionIndex}
total={sortedQuestions.length}
compatibilityQuestion={sortedQuestions[questionIndex]}
question={sortedQuestions[questionIndex]}
user={user}
onSubmit={() => {
setOpen(false)

View File

@@ -8,6 +8,7 @@ import {shortenNumber} from 'common/util/format'
import {sortBy} from 'lodash'
import {useState} from 'react'
import toast from 'react-hot-toast'
import {PinQuestionButton} from 'web/components/answers/pin-question-button'
import {Button} from 'web/components/buttons/button'
import {CompatibilitySort, CompatibilitySortWidget} from 'web/components/compatibility/sort-widget'
import {Col} from 'web/components/layout/col'
@@ -102,7 +103,7 @@ export function getEmptyAnswer(userId: string, questionId: number) {
}
export function AnswerCompatibilityQuestionContent(props: {
compatibilityQuestion: QuestionWithStats
question: QuestionWithStats
user: User
index?: number
total?: number
@@ -114,34 +115,24 @@ export function AnswerCompatibilityQuestionContent(props: {
sort?: CompatibilitySort
setSort?: (sort: CompatibilitySort) => void
}) {
const {
compatibilityQuestion,
user,
onSubmit,
isLastQuestion,
onNext,
noSkip,
index,
total,
sort,
setSort,
} = props
const {question, user, onSubmit, isLastQuestion, onNext, noSkip, index, total, sort, setSort} =
props
const t = useT()
const [answer, setAnswer] = useState<CompatibilityAnswerSubmitType>(
(props.answer as CompatibilityAnswerSubmitType) ??
getEmptyAnswer(user.id, compatibilityQuestion.id),
(props.answer as CompatibilityAnswerSubmitType) ?? getEmptyAnswer(user.id, question.id),
)
const [loading, setLoading] = useState(false)
const [skipLoading, setSkipLoading] = useState(false)
if (
compatibilityQuestion.answer_type !== 'compatibility_multiple_choice' ||
!compatibilityQuestion.multiple_choice_options
question.answer_type !== 'compatibility_multiple_choice' ||
!question.multiple_choice_options
) {
return null
}
const optionOrder = sortBy(Object.entries(compatibilityQuestion.multiple_choice_options), 1).map(
const optionOrder = sortBy(Object.entries(question.multiple_choice_options), 1).map(
([label]) => label,
)
@@ -151,13 +142,11 @@ export function AnswerCompatibilityQuestionContent(props: {
const importanceValid = answer.importance !== null && answer.importance !== -1
const shortenedPopularity = compatibilityQuestion.answer_count
? shortenNumber(compatibilityQuestion.answer_count)
: null
const shortenedPopularity = question.answer_count ? shortenNumber(question.answer_count) : null
return (
<Col className="min-h-0 w-full gap-4">
<Col className="gap-1 shrink-0">
<Row>
<Row className={'gap-2'}>
{isFinite(index!) && isFinite(total!) && (
<span className={'text-sm'}>
<span className="text-ink-600 font-semibold">{index! + 1}</span> / {total}
@@ -173,7 +162,8 @@ export function AnswerCompatibilityQuestionContent(props: {
/>
)}
</Row>
<Row className={''}>
<Row className={'gap-2'}>
<PinQuestionButton questionId={question.id} />
{shortenedPopularity && (
<Tooltip
text={t(
@@ -188,14 +178,14 @@ export function AnswerCompatibilityQuestionContent(props: {
</Row>
</Tooltip>
)}
{isFinite(compatibilityQuestion.community_importance_percent) && (
{isFinite(question.community_importance_percent) && (
<span className={'text-sm ml-auto guidance'}>
{t('compatibility.question.community_importance', 'Community Importance')}:{' '}
{Math.round(compatibilityQuestion.community_importance_percent)}%
{Math.round(question.community_importance_percent)}%
</span>
)}
</Row>
<div data-testid="compatibility-question">{compatibilityQuestion.question}</div>
<div data-testid="compatibility-question">{question.question}</div>
</Col>
<Col className={clsx(SCROLLABLE_MODAL_CLASS, 'w-full gap-4 flex-1 min-h-0 pr-2')}>
<Col className="gap-2">
@@ -257,7 +247,7 @@ export function AnswerCompatibilityQuestionContent(props: {
disabled={loading || skipLoading}
onClick={() => {
setSkipLoading(true)
submitCompatibilityAnswer(getEmptyAnswer(user.id, compatibilityQuestion.id))
submitCompatibilityAnswer(getEmptyAnswer(user.id, question.id))
.then(() => {
if (isLastQuestion) {
onSubmit()

View File

@@ -12,8 +12,10 @@ import {Row as rowFor} from 'common/supabase/utils'
import {User} from 'common/user'
import {shortenNumber} from 'common/util/format'
import {keyBy, partition, sortBy} from 'lodash'
import {PinIcon} from 'lucide-react'
import {useCallback, useEffect, useMemo, useState} from 'react'
import toast from 'react-hot-toast'
import {AddCompatibilityQuestionButton} from 'web/components/answers/add-compatibility-question-button'
import DropdownMenu from 'web/components/comments/dropdown-menu'
import {
compareBySort,
@@ -32,6 +34,7 @@ import {Tooltip} from 'web/components/widgets/tooltip'
import {shortenName} from 'web/components/widgets/user-link'
import {useIsLooking} from 'web/hooks/use-is-looking'
import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state'
import {usePinnedQuestionIds} from 'web/hooks/use-pinned-question-ids'
import {useProfile} from 'web/hooks/use-profile'
import {useCompatibleProfiles} from 'web/hooks/use-profiles'
import {
@@ -43,7 +46,6 @@ import {useT} from 'web/lib/locale'
import {db} from 'web/lib/supabase/db'
import {Subtitle} from '../widgets/profile-subtitle'
import {AddCompatibilityQuestionButton} from './add-compatibility-question-button'
import {
AnswerCompatibilityQuestionButton,
AnswerSkippedCompatibilityQuestionsButton,
@@ -59,8 +61,10 @@ import {
submitCompatibilityAnswer,
} from './answer-compatibility-question-content'
import {PreferredList, PreferredListNoComparison} from './compatibility-question-preferred-list'
import {PinQuestionButton} from './pin-question-button'
const NUM_QUESTIONS_TO_SHOW = 8
const NUM_QUESTIONS_TO_SHOW = 4
const NUM_PINNED_QUESTIONS_TO_SHOW = 4
export function separateQuestionsArray(
questions: QuestionWithStats[],
@@ -101,6 +105,8 @@ export function CompatibilityQuestionsDisplay(props: {
const compatibleProfiles = useCompatibleProfiles(currentUser?.id)
const compatibilityScore = compatibleProfiles?.profileCompatibilityScores?.[profile.user_id]
const {pinnedQuestionIds, refreshPinnedQuestionIds} = usePinnedQuestionIds()
const {refreshCompatibilityQuestions, compatibilityQuestions} =
useCompatibilityQuestionsWithAnswerCount()
@@ -125,7 +131,8 @@ export function CompatibilityQuestionsDisplay(props: {
const refreshCompatibilityAll = useCallback(() => {
refreshCompatibilityAnswers()
refreshCompatibilityQuestions()
}, [refreshCompatibilityAnswers, refreshCompatibilityQuestions])
refreshPinnedQuestionIds()
}, [refreshCompatibilityAnswers, refreshCompatibilityQuestions, refreshPinnedQuestionIds])
const isLooking = useIsLooking()
const [sort, setSort] = usePersistentInMemoryState<CompatibilitySort>(
@@ -184,23 +191,68 @@ export function CompatibilityQuestionsDisplay(props: {
currentSlice + NUM_QUESTIONS_TO_SHOW,
)
const pinnedAnswers = useMemo(() => {
if (!pinnedQuestionIds?.length) return []
const pinned = answers.filter((a) => pinnedQuestionIds.includes(a.question_id))
const idToIndex = new Map(pinnedQuestionIds.map((id, i) => [id, i] as const))
return sortBy(pinned, (a) => idToIndex.get(a.question_id) ?? Infinity)
}, [answers, pinnedQuestionIds])
const [pinnedPage, setPinnedPage] = useState(0)
const pinnedCurrentSlice = pinnedPage * NUM_PINNED_QUESTIONS_TO_SHOW
const shownPinnedAnswers = pinnedAnswers.slice(
pinnedCurrentSlice,
pinnedCurrentSlice + NUM_PINNED_QUESTIONS_TO_SHOW,
)
useEffect(() => {
setPinnedPage(0)
}, [user.id])
if (!isCurrentUser && !answeredQuestions.length) return null
return (
<Col className="gap-4">
<Row className="flex-wrap items-center justify-between gap-x-6 gap-y-4">
<Row className={'gap-8'}>
<Subtitle>
{isCurrentUser
? t('answers.display.your_prompts', 'Your Compatibility Prompts')
: t('answers.display.user_prompts', "{name}'s Compatibility Prompts", {
name: shortenName(user.name),
})}
</Subtitle>
{compatibilityScore && (
<CompatibleBadge compatibility={compatibilityScore} className={'mt-7 mr-4'} />
<Row className={'gap-8'}>
<Subtitle>
{isCurrentUser
? t('answers.display.your_prompts', 'Your Compatibility Prompts')
: t('answers.display.user_prompts', "{name}'s Compatibility Prompts", {
name: shortenName(user.name),
})}
</Subtitle>
{compatibilityScore && (
<CompatibleBadge compatibility={compatibilityScore} className={'mt-7 mr-4'} />
)}
</Row>
{pinnedAnswers.length > 0 && (
<Col className="gap-3">
<PinIcon />
{shownPinnedAnswers.map((answer) => (
<CompatibilityAnswerBlock
key={`pinned-${answer.question_id}`}
answer={answer}
yourQuestions={answeredQuestions}
user={user}
isCurrentUser={isCurrentUser}
refreshCompatibilityAll={refreshCompatibilityAll}
profile={profile}
fromProfilePage={fromProfilePage}
showCommunityInfo={showCommunityInfo}
/>
))}
{NUM_PINNED_QUESTIONS_TO_SHOW < pinnedAnswers.length && (
<Pagination
page={pinnedPage}
pageSize={NUM_PINNED_QUESTIONS_TO_SHOW}
totalItems={pinnedAnswers.length}
setPage={setPinnedPage}
/>
)}
</Row>
<div className="border-canvas-200 border-b" />
</Col>
)}
<Row className="flex-wrap items-center justify-between gap-x-6 gap-y-4">
{answeredQuestions.length > 0 && (
<div className="relative mt-3">
{/*<input*/}
@@ -254,26 +306,6 @@ export function CompatibilityQuestionsDisplay(props: {
</span>
) : (
<>
{isCurrentUser && !fromProfilePage && (
<span className="custom-link">
{otherQuestions.length < 1 ? (
<span className="text-ink-600 text-sm">
{t(
'answers.display.already_answered_all',
"You've already answered all the compatibility questions—",
)}
</span>
) : (
<span className="text-ink-600 text-sm">
{t(
'answers.display.answer_more',
'Answer more questions to increase your compatibility scores—or ',
)}
</span>
)}
<AddCompatibilityQuestionButton refreshCompatibilityAll={refreshCompatibilityAll} />
</span>
)}
{shownAnswers.map((answer) => {
return (
<CompatibilityAnswerBlock
@@ -294,6 +326,34 @@ export function CompatibilityQuestionsDisplay(props: {
)}
</>
)}
{NUM_QUESTIONS_TO_SHOW < answers.length && (
<Pagination
page={page}
pageSize={NUM_QUESTIONS_TO_SHOW}
totalItems={sortedAndFilteredAnswers.length}
setPage={setPage}
/>
)}
{isCurrentUser && !fromProfilePage && (
<span className="custom-link">
{otherQuestions.length < 1 ? (
<span className="text-ink-600 text-sm">
{t(
'answers.display.already_answered_all',
"You've already answered all the compatibility questions—",
)}
</span>
) : (
<span className="text-ink-600 text-sm">
{t(
'answers.display.answer_more',
'Answer more questions to increase your compatibility scores—or ',
)}
</span>
)}
<AddCompatibilityQuestionButton refreshCompatibilityAll={refreshCompatibilityAll} />
</span>
)}
{isCurrentUser && (
<Row className={'w-full justify-center gap-8'}>
{(fromSignup || (otherQuestions.length >= 1 && !fromProfilePage)) && (
@@ -316,14 +376,6 @@ export function CompatibilityQuestionsDisplay(props: {
/>
</Row>
)}
{NUM_QUESTIONS_TO_SHOW < answers.length && (
<Pagination
page={page}
pageSize={NUM_QUESTIONS_TO_SHOW}
totalItems={sortedAndFilteredAnswers.length}
setPage={setPage}
/>
)}
</Col>
)
}
@@ -421,6 +473,7 @@ export function CompatibilityAnswerBlock(props: {
/>
</div>
)}
{!!currentUser && <PinQuestionButton questionId={question.id} />}
{isCurrentUser && isAnswered && (
<>
<ImportanceButton
@@ -577,7 +630,7 @@ export function CompatibilityAnswerBlock(props: {
<Col className={MODAL_CLASS}>
<AnswerCompatibilityQuestionContent
key={`edit answer.id`}
compatibilityQuestion={question}
question={question}
answer={newAnswer}
user={user}
onSubmit={() => {

View File

@@ -0,0 +1,68 @@
import {BookmarkIcon as PinOutline} from '@heroicons/react/24/outline'
import {BookmarkIcon as PinSolid} from '@heroicons/react/24/solid'
import clsx from 'clsx'
import toast from 'react-hot-toast'
import {Tooltip} from 'web/components/widgets/tooltip'
import {usePinnedQuestionIds} from 'web/hooks/use-pinned-question-ids'
import {api} from 'web/lib/api'
import {useT} from 'web/lib/locale'
interface PinQuestionButtonProps {
questionId: number
onPinChange?: (pinnedQuestionIds: number[]) => void
className?: string
}
export function PinQuestionButton({questionId, onPinChange, className}: PinQuestionButtonProps) {
const t = useT()
const {pinnedQuestionIds, setPinnedQuestionIds, refreshPinnedQuestionIds} = usePinnedQuestionIds()
const isPinned = (pinnedQuestionIds ?? []).includes(questionId)
const handlePinToggle = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
api('update-compatibility-question-pin', {
questionId,
pinned: !isPinned,
})
.then((res) => {
refreshPinnedQuestionIds()
setPinnedQuestionIds(res.pinnedQuestionIds ?? [])
onPinChange?.(res.pinnedQuestionIds ?? [])
})
.catch((err) => {
console.error(err)
toast.error(err.message ?? t('answers.pinned.error', 'Error updating preference'))
})
}
return (
<Tooltip
text={
isPinned
? t('answers.pinned.unpin_tooltip', 'Unpin this question')
: t('answers.pinned.pin_tooltip', 'Pin this question to see it first on profiles')
}
>
<button
className={clsx(
'rounded transition-colors',
isPinned ? 'text-primary-700' : 'text-ink-400 hover:text-ink-700',
className,
)}
onClick={handlePinToggle}
aria-label={
isPinned
? t('answers.pinned.unpin_aria', 'Unpin question')
: t('answers.pinned.pin_aria', 'Pin question')
}
>
{isPinned ? (
<PinSolid className="h-5 w-5 text-primary-600" />
) : (
<PinOutline className="h-5 w-5 text-ink-500" />
)}
</button>
</Tooltip>
)
}

View File

@@ -0,0 +1,36 @@
import {createContext, ReactNode, useCallback, useContext, useEffect, useState} from 'react'
import {api} from 'web/lib/api'
const PinnedQuestionIdsContext = createContext<{
pinnedQuestionIds: number[] | undefined
setPinnedQuestionIds: (ids: number[]) => void
refreshPinnedQuestionIds: () => void
} | null>(null)
export function PinnedQuestionIdsProvider({children}: {children: ReactNode}) {
const [pinnedQuestionIds, setPinnedQuestionIds] = useState<number[] | undefined>(undefined)
const refreshPinnedQuestionIds = useCallback(() => {
api('get-pinned-compatibility-questions').then((res) => {
setPinnedQuestionIds(res.pinnedQuestionIds ?? [])
})
}, [])
useEffect(() => {
refreshPinnedQuestionIds()
}, [])
return (
<PinnedQuestionIdsContext.Provider
value={{pinnedQuestionIds, setPinnedQuestionIds, refreshPinnedQuestionIds}}
>
{children}
</PinnedQuestionIdsContext.Provider>
)
}
export function usePinnedQuestionIds() {
const ctx = useContext(PinnedQuestionIdsContext)
if (!ctx) throw new Error('usePinnedQuestionIds must be used within PinnedQuestionIdsProvider')
return ctx
}

View File

@@ -1,5 +1,5 @@
import {isEqual} from 'lodash'
import {SetStateAction, useMemo, useRef, useState} from 'react'
import {SetStateAction, useCallback, useRef, useState} from 'react'
export const useStateCheckEquality = <T>(initialState: T) => {
const [state, setState] = useState(initialState)
@@ -7,8 +7,8 @@ export const useStateCheckEquality = <T>(initialState: T) => {
const stateRef = useRef(state)
stateRef.current = state
const checkSetState = useMemo(
() => (next: SetStateAction<T>) => {
const checkSetState = useCallback(
(next: SetStateAction<T>) => {
const state = stateRef.current
const newState = next instanceof Function ? next(state) : next
if (!isEqual(state, newState)) {

View File

@@ -24,6 +24,7 @@ import {ChoicesProvider} from 'web/hooks/use-choices'
import {useFontPreferenceManager} from 'web/hooks/use-font-preference'
import {useHasLoaded} from 'web/hooks/use-has-loaded'
import {HiddenProfilesProvider} from 'web/hooks/use-hidden-profiles'
import {PinnedQuestionIdsProvider} from 'web/hooks/use-pinned-question-ids'
import {updateStatusBar} from 'web/hooks/use-theme'
import {updateBackendLocale} from 'web/lib/api'
import {DAYJS_LOCALE_IMPORTS, registerDatePickerLocale} from 'web/lib/dayjs'
@@ -198,11 +199,13 @@ function MyApp(props: AppProps<PageProps>) {
<ErrorBoundary>
<AuthProvider serverUser={pageProps.auth}>
<ChoicesProvider>
<HiddenProfilesProvider>
<WebPush />
<AndroidPush />
<Component {...pageProps} />
</HiddenProfilesProvider>
<PinnedQuestionIdsProvider>
<HiddenProfilesProvider>
<WebPush />
<AndroidPush />
<Component {...pageProps} />
</HiddenProfilesProvider>
</PinnedQuestionIdsProvider>
</ChoicesProvider>
</AuthProvider>
</ErrorBoundary>