From bdafa43472fbb565168e2c44b4279e3299b9d889 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Thu, 19 Mar 2026 17:00:34 +0100 Subject: [PATCH] Add pinned compatibility questions feature with backend support and UI integration --- backend/api/src/app.ts | 6 +- .../src/get-pinned-compatibility-questions.ts | 22 +++ .../src/update-compatibility-question-pin.ts | 28 ++++ backend/supabase/migration.sql | 1 + ...60319_add_compatibility_prompts_pinned.sql | 19 +++ common/messages/de.json | 6 + common/messages/fr.json | 6 + common/src/api/schema.ts | 29 ++++ common/src/supabase/schema.ts | 36 +++++ .../add-compatibility-question-button.tsx | 2 +- .../answer-compatibility-question-button.tsx | 2 +- .../answer-compatibility-question-content.tsx | 44 +++--- .../compatibility-questions-display.tsx | 141 ++++++++++++------ .../answers/pin-question-button.tsx | 68 +++++++++ web/hooks/use-pinned-question-ids.tsx | 36 +++++ web/hooks/use-state-check-equality.ts | 6 +- web/pages/_app.tsx | 13 +- 17 files changed, 383 insertions(+), 82 deletions(-) create mode 100644 backend/api/src/get-pinned-compatibility-questions.ts create mode 100644 backend/api/src/update-compatibility-question-pin.ts create mode 100644 backend/supabase/migrations/20260319_add_compatibility_prompts_pinned.sql create mode 100644 web/components/answers/pin-question-button.tsx create mode 100644 web/hooks/use-pinned-question-ids.tsx diff --git a/backend/api/src/app.ts b/backend/api/src/app.ts index 2fb3f770..cbcb3d5a 100644 --- a/backend/api/src/app.ts +++ b/backend/api/src/app.ts @@ -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} = { '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, diff --git a/backend/api/src/get-pinned-compatibility-questions.ts b/backend/api/src/get-pinned-compatibility-questions.ts new file mode 100644 index 00000000..ed006e9d --- /dev/null +++ b/backend/api/src/get-pinned-compatibility-questions.ts @@ -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>( + `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} +} diff --git a/backend/api/src/update-compatibility-question-pin.ts b/backend/api/src/update-compatibility-question-pin.ts new file mode 100644 index 00000000..03d96711 --- /dev/null +++ b/backend/api/src/update-compatibility-question-pin.ts @@ -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} +} diff --git a/backend/supabase/migration.sql b/backend/supabase/migration.sql index 8cf95e0f..03b03eb6 100644 --- a/backend/supabase/migration.sql +++ b/backend/supabase/migration.sql @@ -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; diff --git a/backend/supabase/migrations/20260319_add_compatibility_prompts_pinned.sql b/backend/supabase/migrations/20260319_add_compatibility_prompts_pinned.sql new file mode 100644 index 00000000..eb7ce4a2 --- /dev/null +++ b/backend/supabase/migrations/20260319_add_compatibility_prompts_pinned.sql @@ -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); + diff --git a/common/messages/de.json b/common/messages/de.json index 007a09fd..ee414a12 100644 --- a/common/messages/de.json +++ b/common/messages/de.json @@ -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}", diff --git a/common/messages/fr.json b/common/messages/fr.json index 49468da5..78153e32 100644 --- a/common/messages/fr.json +++ b/common/messages/fr.json @@ -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}", diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index c815a15b..094d9f31 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -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, diff --git a/common/src/supabase/schema.ts b/common/src/supabase/schema.ts index 8638207c..4174f830 100644 --- a/common/src/supabase/schema.ts +++ b/common/src/supabase/schema.ts @@ -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 diff --git a/web/components/answers/add-compatibility-question-button.tsx b/web/components/answers/add-compatibility-question-button.tsx index c50e2d71..80ab85ab 100644 --- a/web/components/answers/add-compatibility-question-button.tsx +++ b/web/components/answers/add-compatibility-question-button.tsx @@ -64,7 +64,7 @@ function AddCompatibilityQuestionModal(props: { ) : ( { // setOpen(false) diff --git a/web/components/answers/answer-compatibility-question-button.tsx b/web/components/answers/answer-compatibility-question-button.tsx index fae9b8d1..b9b5ad27 100644 --- a/web/components/answers/answer-compatibility-question-button.tsx +++ b/web/components/answers/answer-compatibility-question-button.tsx @@ -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) diff --git a/web/components/answers/answer-compatibility-question-content.tsx b/web/components/answers/answer-compatibility-question-content.tsx index d40af5e1..4b4ae371 100644 --- a/web/components/answers/answer-compatibility-question-content.tsx +++ b/web/components/answers/answer-compatibility-question-content.tsx @@ -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( - (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 ( - + {isFinite(index!) && isFinite(total!) && ( {index! + 1} / {total} @@ -173,7 +162,8 @@ export function AnswerCompatibilityQuestionContent(props: { /> )} - + + {shortenedPopularity && ( )} - {isFinite(compatibilityQuestion.community_importance_percent) && ( + {isFinite(question.community_importance_percent) && ( {t('compatibility.question.community_importance', 'Community Importance')}:{' '} - {Math.round(compatibilityQuestion.community_importance_percent)}% + {Math.round(question.community_importance_percent)}% )} -
{compatibilityQuestion.question}
+
{question.question}
@@ -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() diff --git a/web/components/answers/compatibility-questions-display.tsx b/web/components/answers/compatibility-questions-display.tsx index 9afc2251..35eb2839 100644 --- a/web/components/answers/compatibility-questions-display.tsx +++ b/web/components/answers/compatibility-questions-display.tsx @@ -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( @@ -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 ( - - - - {isCurrentUser - ? t('answers.display.your_prompts', 'Your Compatibility Prompts') - : t('answers.display.user_prompts', "{name}'s Compatibility Prompts", { - name: shortenName(user.name), - })} - - {compatibilityScore && ( - + + + {isCurrentUser + ? t('answers.display.your_prompts', 'Your Compatibility Prompts') + : t('answers.display.user_prompts', "{name}'s Compatibility Prompts", { + name: shortenName(user.name), + })} + + {compatibilityScore && ( + + )} + + {pinnedAnswers.length > 0 && ( + + + {shownPinnedAnswers.map((answer) => ( + + ))} + {NUM_PINNED_QUESTIONS_TO_SHOW < pinnedAnswers.length && ( + )} - +
+ + )} + {answeredQuestions.length > 0 && (
{/* ) : ( <> - {isCurrentUser && !fromProfilePage && ( - - {otherQuestions.length < 1 ? ( - - {t( - 'answers.display.already_answered_all', - "You've already answered all the compatibility questions—", - )} - - ) : ( - - {t( - 'answers.display.answer_more', - 'Answer more questions to increase your compatibility scores—or ', - )} - - )} - - - )} {shownAnswers.map((answer) => { return ( )} + {NUM_QUESTIONS_TO_SHOW < answers.length && ( + + )} + {isCurrentUser && !fromProfilePage && ( + + {otherQuestions.length < 1 ? ( + + {t( + 'answers.display.already_answered_all', + "You've already answered all the compatibility questions—", + )} + + ) : ( + + {t( + 'answers.display.answer_more', + 'Answer more questions to increase your compatibility scores—or ', + )} + + )} + + + )} {isCurrentUser && ( {(fromSignup || (otherQuestions.length >= 1 && !fromProfilePage)) && ( @@ -316,14 +376,6 @@ export function CompatibilityQuestionsDisplay(props: { /> )} - {NUM_QUESTIONS_TO_SHOW < answers.length && ( - - )} ) } @@ -421,6 +473,7 @@ export function CompatibilityAnswerBlock(props: { />
)} + {!!currentUser && } {isCurrentUser && isAnswered && ( <> { diff --git a/web/components/answers/pin-question-button.tsx b/web/components/answers/pin-question-button.tsx new file mode 100644 index 00000000..573accf9 --- /dev/null +++ b/web/components/answers/pin-question-button.tsx @@ -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 ( + + + + ) +} diff --git a/web/hooks/use-pinned-question-ids.tsx b/web/hooks/use-pinned-question-ids.tsx new file mode 100644 index 00000000..f4783257 --- /dev/null +++ b/web/hooks/use-pinned-question-ids.tsx @@ -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(undefined) + + const refreshPinnedQuestionIds = useCallback(() => { + api('get-pinned-compatibility-questions').then((res) => { + setPinnedQuestionIds(res.pinnedQuestionIds ?? []) + }) + }, []) + + useEffect(() => { + refreshPinnedQuestionIds() + }, []) + + return ( + + {children} + + ) +} + +export function usePinnedQuestionIds() { + const ctx = useContext(PinnedQuestionIdsContext) + if (!ctx) throw new Error('usePinnedQuestionIds must be used within PinnedQuestionIdsProvider') + return ctx +} diff --git a/web/hooks/use-state-check-equality.ts b/web/hooks/use-state-check-equality.ts index 2ac7207b..e94c3f1b 100644 --- a/web/hooks/use-state-check-equality.ts +++ b/web/hooks/use-state-check-equality.ts @@ -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 = (initialState: T) => { const [state, setState] = useState(initialState) @@ -7,8 +7,8 @@ export const useStateCheckEquality = (initialState: T) => { const stateRef = useRef(state) stateRef.current = state - const checkSetState = useMemo( - () => (next: SetStateAction) => { + const checkSetState = useCallback( + (next: SetStateAction) => { const state = stateRef.current const newState = next instanceof Function ? next(state) : next if (!isEqual(state, newState)) { diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index 514df419..faaeed55 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -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) { - - - - - + + + + + + +