mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-02-23 18:36:02 -05:00
Make answers to compatibility prompts searchable
This commit is contained in:
@@ -3,20 +3,42 @@ import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||
import {Row} from 'common/supabase/utils'
|
||||
|
||||
export function shuffle<T>(array: T[]): T[] {
|
||||
const arr = [...array]; // copy to avoid mutating the original
|
||||
const arr = [...array] // copy to avoid mutating the original
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[arr[i], arr[j]] = [arr[j], arr[i]]
|
||||
}
|
||||
return arr;
|
||||
return arr
|
||||
}
|
||||
|
||||
export const getCompatibilityQuestions: APIHandler<
|
||||
'get-compatibility-questions'
|
||||
> = async (props, _auth) => {
|
||||
const {locale = 'en'} = props
|
||||
const {locale = 'en', keyword} = props
|
||||
const pg = createSupabaseDirectClient()
|
||||
|
||||
// Build query parameters
|
||||
const params: (string | number)[] = [locale]
|
||||
const paramIndex = 2
|
||||
|
||||
// Build keyword filter condition - search in question text and multiple_choice_options keys
|
||||
const keywordFilter = keyword
|
||||
? `AND (
|
||||
COALESCE(cpt.question, cp.question) ILIKE $${paramIndex}
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM jsonb_object_keys(
|
||||
COALESCE(cpt.multiple_choice_options, cp.multiple_choice_options)
|
||||
) AS option_key
|
||||
WHERE option_key ILIKE $${paramIndex}
|
||||
)
|
||||
)`
|
||||
: ''
|
||||
|
||||
if (keyword) {
|
||||
params.push(`%${keyword}%`)
|
||||
}
|
||||
|
||||
const questions = await pg.manyOrNone<
|
||||
Row<'compatibility_prompts'> & { answer_count: number; score: number }
|
||||
>(
|
||||
@@ -52,6 +74,7 @@ export const getCompatibilityQuestions: APIHandler<
|
||||
AND $1 <> 'en'
|
||||
|
||||
WHERE cp.answer_type = 'compatibility_multiple_choice'
|
||||
${keywordFilter}
|
||||
|
||||
GROUP BY cp.id,
|
||||
cpt.question,
|
||||
@@ -59,7 +82,7 @@ export const getCompatibilityQuestions: APIHandler<
|
||||
|
||||
ORDER BY cp.importance_score
|
||||
`,
|
||||
[locale]
|
||||
params
|
||||
)
|
||||
|
||||
// console.debug({questions})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {arraybeSchema, baseProfilesSchema, combinedProfileSchema, contentSchema, zBoolean,} from 'common/api/zod-types'
|
||||
import {PrivateChatMessage} from 'common/chat-message'
|
||||
import {CompatibilityScore} from 'common/profiles/compatibility-score'
|
||||
import {MAX_COMPATIBILITY_QUESTION_LENGTH, OPTION_TABLES} from 'common/profiles/constants'
|
||||
import {MAX_COMPATIBILITY_QUESTION_LENGTH, OPTION_TABLES,} from 'common/profiles/constants'
|
||||
import {Profile, ProfileRow} from 'common/profiles/profile'
|
||||
import {Row} from 'common/supabase/utils'
|
||||
import {PrivateUser, User} from 'common/user'
|
||||
@@ -49,7 +49,12 @@ export const API = (_apiTypeCheck = {
|
||||
message: 'Server is working.'
|
||||
uid?: string
|
||||
version?: string
|
||||
git?: { revision?: string; commitDate?: string; author?: string, message?: string }
|
||||
git?: {
|
||||
revision?: string
|
||||
commitDate?: string
|
||||
author?: string
|
||||
message?: string
|
||||
}
|
||||
},
|
||||
summary: 'Check whether the API server is running',
|
||||
tag: 'General',
|
||||
@@ -342,7 +347,8 @@ export const API = (_apiTypeCheck = {
|
||||
authed: true,
|
||||
rateLimited: false,
|
||||
props: z.object({
|
||||
locale: z.string().optional()
|
||||
locale: z.string().optional(),
|
||||
keyword: z.string().optional(),
|
||||
}),
|
||||
returns: {} as {
|
||||
status: 'success'
|
||||
@@ -470,10 +476,12 @@ export const API = (_apiTypeCheck = {
|
||||
method: 'GET',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
props: z.object({
|
||||
limit: z.coerce.number().min(1).max(200).optional(),
|
||||
offset: z.coerce.number().min(0).optional(),
|
||||
}).strict(),
|
||||
props: z
|
||||
.object({
|
||||
limit: z.coerce.number().min(1).max(200).optional(),
|
||||
offset: z.coerce.number().min(0).optional(),
|
||||
})
|
||||
.strict(),
|
||||
returns: {} as {
|
||||
status: 'success'
|
||||
hidden: {
|
||||
@@ -545,8 +553,8 @@ export const API = (_apiTypeCheck = {
|
||||
.strict(),
|
||||
returns: {} as {
|
||||
status: 'success' | 'fail'
|
||||
profiles: Profile[],
|
||||
count: number,
|
||||
profiles: Profile[]
|
||||
count: number
|
||||
},
|
||||
summary: 'List profiles with filters, pagination and ordering',
|
||||
tag: 'Profiles',
|
||||
@@ -788,7 +796,7 @@ export const API = (_apiTypeCheck = {
|
||||
summary: 'Create a new vote/poll',
|
||||
tag: 'Votes',
|
||||
},
|
||||
'vote': {
|
||||
vote: {
|
||||
method: 'POST',
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
@@ -825,7 +833,7 @@ export const API = (_apiTypeCheck = {
|
||||
summary: 'Find places near a GeoDB city ID within a radius',
|
||||
tag: 'Locations',
|
||||
},
|
||||
'contact': {
|
||||
contact: {
|
||||
method: 'POST',
|
||||
authed: false,
|
||||
rateLimited: true,
|
||||
@@ -852,7 +860,7 @@ export const API = (_apiTypeCheck = {
|
||||
rateLimited: true,
|
||||
returns: {} as any,
|
||||
props: z.object({
|
||||
subscription: z.record(z.any())
|
||||
subscription: z.record(z.any()),
|
||||
}),
|
||||
summary: 'Save a push/browser subscription for the user',
|
||||
tag: 'Notifications',
|
||||
@@ -873,12 +881,11 @@ export const API = (_apiTypeCheck = {
|
||||
authed: true,
|
||||
rateLimited: true,
|
||||
returns: {} as Row<'bookmarked_searches'>,
|
||||
props: z
|
||||
.object({
|
||||
search_filters: z.any().optional(),
|
||||
location: z.any().optional(),
|
||||
search_name: z.string().nullable().optional(),
|
||||
}),
|
||||
props: z.object({
|
||||
search_filters: z.any().optional(),
|
||||
location: z.any().optional(),
|
||||
search_name: z.string().nullable().optional(),
|
||||
}),
|
||||
summary: 'Create a bookmarked search for quick reuse',
|
||||
tag: 'Searches',
|
||||
},
|
||||
@@ -988,11 +995,12 @@ export type ValidatedAPIParams<N extends APIPath> = z.output<
|
||||
APISchema<N>['props']
|
||||
>
|
||||
|
||||
export type APIResponse<N extends APIPath> = APISchema<N> extends {
|
||||
returns: Record<string, any>
|
||||
}
|
||||
? APISchema<N>['returns']
|
||||
: void
|
||||
export type APIResponse<N extends APIPath> =
|
||||
APISchema<N> extends {
|
||||
returns: Record<string, any>
|
||||
}
|
||||
? APISchema<N>['returns']
|
||||
: void
|
||||
|
||||
export type APIResponseOptionalContinue<N extends APIPath> =
|
||||
| { continue: () => Promise<void>; result: APIResponse<N> }
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'web/lib/supabase/questions'
|
||||
import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state'
|
||||
import {api} from 'web/lib/api'
|
||||
import {useLocale} from "web/lib/locale";
|
||||
import {useLocale} from 'web/lib/locale'
|
||||
|
||||
export const useQuestions = () => {
|
||||
const [questions, setQuestions] = useState<Row<'compatibility_prompts'>[]>([])
|
||||
@@ -44,7 +44,7 @@ export const useUserAnswers = (userId: string | undefined) => {
|
||||
getUserAnswers(userId).then(setAnswers)
|
||||
}
|
||||
|
||||
return { refreshAnswers, answers }
|
||||
return {refreshAnswers, answers}
|
||||
}
|
||||
|
||||
export const useUserCompatibilityAnswers = (userId: string | undefined) => {
|
||||
@@ -72,7 +72,7 @@ export const useUserCompatibilityAnswers = (userId: string | undefined) => {
|
||||
getUserCompatibilityAnswers(userId).then(setCompatibilityAnswers)
|
||||
}
|
||||
|
||||
return { refreshCompatibilityAnswers, compatibilityAnswers }
|
||||
return {refreshCompatibilityAnswers, compatibilityAnswers}
|
||||
}
|
||||
|
||||
export type QuestionWithCountType = Row<'compatibility_prompts'> & {
|
||||
@@ -93,26 +93,32 @@ export const useFRQuestionsWithAnswerCount = () => {
|
||||
return FRquestionsWithCount as QuestionWithCountType[]
|
||||
}
|
||||
|
||||
export const useCompatibilityQuestionsWithAnswerCount = () => {
|
||||
export const useCompatibilityQuestionsWithAnswerCount = (keyword?: string) => {
|
||||
const {locale} = useLocale()
|
||||
const [compatibilityQuestions, setCompatibilityQuestions] =
|
||||
usePersistentInMemoryState<QuestionWithCountType[]>(
|
||||
[],
|
||||
`compatibility-questions-with-count`
|
||||
)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
async function refreshCompatibilityQuestions() {
|
||||
return api('get-compatibility-questions', {locale}).then((res) => {
|
||||
setCompatibilityQuestions(res.questions)
|
||||
})
|
||||
setIsLoading(true)
|
||||
return api('get-compatibility-questions', {locale, keyword}).then(
|
||||
(res) => {
|
||||
setCompatibilityQuestions(res.questions)
|
||||
setIsLoading(false)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshCompatibilityQuestions()
|
||||
}, [locale])
|
||||
}, [locale, keyword])
|
||||
|
||||
return {
|
||||
refreshCompatibilityQuestions,
|
||||
compatibilityQuestions,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,9 @@
|
||||
"compatibility.tabs.to_answer": "Zu beantworten",
|
||||
"compatibility.title": "Ihre Kompatibilitätsfragen",
|
||||
"compatibility.tooltip": "Kompatibilitätswert zwischen Ihnen beiden",
|
||||
"compatibility.empty.no_results": "Keine Ergebnisse für \"{keyword}\"",
|
||||
"compatibility.seo.title": "Kompatibilität",
|
||||
"compatibility.seo.description": "Ihre Kompatibilitätsfragen anzeigen und verwalten",
|
||||
"contact.editor.placeholder": "Kontaktieren Sie uns hier...",
|
||||
"contact.form_link": "Feedback-Formular",
|
||||
"contact.intro_middle": " oder über unsere ",
|
||||
@@ -1136,5 +1139,6 @@
|
||||
"filter.label.diet": "Ernährung",
|
||||
"filter.label.political_beliefs": "Politische Ansichten",
|
||||
"filter.label.mbti": "MBTI",
|
||||
"filter.drinks.per_month": "pro Monat"
|
||||
"filter.drinks.per_month": "pro Monat",
|
||||
"compatibility.search_placeholder": "Fragen und Antworten durchsuchen..."
|
||||
}
|
||||
|
||||
@@ -1136,5 +1136,9 @@
|
||||
"filter.label.diet": "Régime",
|
||||
"filter.label.political_beliefs": "Opinions politiques",
|
||||
"filter.label.mbti": "MBTI",
|
||||
"filter.drinks.per_month": "par mois"
|
||||
"filter.drinks.per_month": "par mois",
|
||||
"compatibility.search_placeholder": "Rechercher des questions et réponses...",
|
||||
"compatibility.seo.title": "Compatibilité",
|
||||
"compatibility.seo.description": "Afficher et gérer vos questions de compatibilité",
|
||||
"compatibility.empty.no_results": "Aucun résultat pour \"{keyword}\""
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import {useUser} from 'web/hooks/use-user'
|
||||
import {useCompatibilityQuestionsWithAnswerCount, useUserCompatibilityAnswers} from 'web/hooks/use-questions'
|
||||
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||
import {useCompatibilityQuestionsWithAnswerCount, useUserCompatibilityAnswers,} from 'web/hooks/use-questions'
|
||||
import {useCallback, useEffect, useMemo, useRef, 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";
|
||||
import {LoadMoreUntilNotVisible} from "web/components/widgets/visibility-observer";
|
||||
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'
|
||||
import {LoadMoreUntilNotVisible} from 'web/components/widgets/visibility-observer'
|
||||
import {useT} from 'web/lib/locale'
|
||||
import {Input} from 'web/components/widgets/input'
|
||||
import {debounce} from 'lodash'
|
||||
import clsx from "clsx";
|
||||
import {SEO} from "web/components/SEO";
|
||||
|
||||
type QuestionWithAnswer = Question & {
|
||||
answer?: Row<'compatibility_answers'>
|
||||
@@ -24,11 +28,27 @@ 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 [keyword, setKeyword] = useState('')
|
||||
const [debouncedKeyword, setDebouncedKeyword] = useState('')
|
||||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||||
const {compatibilityAnswers, refreshCompatibilityAnswers} =
|
||||
useUserCompatibilityAnswers(user?.id)
|
||||
const {compatibilityQuestions, refreshCompatibilityQuestions, isLoading} =
|
||||
useCompatibilityQuestionsWithAnswerCount(debouncedKeyword || undefined)
|
||||
const t = useT()
|
||||
|
||||
// Debounce keyword changes
|
||||
const debouncedSetKeyword = useMemo(
|
||||
() => debounce((value: string) => setDebouncedKeyword(value), 500),
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
debouncedSetKeyword(keyword)
|
||||
// Cleanup debounce on unmount
|
||||
return () => debouncedSetKeyword.cancel()
|
||||
}, [keyword, debouncedSetKeyword])
|
||||
|
||||
const questionsWithAnswers = useMemo(() => {
|
||||
if (!compatibilityQuestions) return []
|
||||
|
||||
@@ -36,12 +56,14 @@ export default function CompatibilityPage() {
|
||||
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[]
|
||||
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(() => {
|
||||
@@ -69,7 +91,7 @@ export default function CompatibilityPage() {
|
||||
Promise.all([
|
||||
refreshCompatibilityAnswers(),
|
||||
refreshCompatibilityQuestions(),
|
||||
]).finally(() => setIsLoading(false))
|
||||
]).finally(() => console.log('refreshed compatibility'))
|
||||
}
|
||||
}, [user?.id])
|
||||
|
||||
@@ -80,15 +102,34 @@ export default function CompatibilityPage() {
|
||||
|
||||
return (
|
||||
<PageBase trackPageView={'compatibility'}>
|
||||
{user ?
|
||||
<SEO
|
||||
title={t('compatibility.seo.title', 'Compatibility')}
|
||||
description={t('compatibility.seo.description', 'View and manage your compatibility questions')}
|
||||
url={`/compatibility`}
|
||||
/>
|
||||
{user ? (
|
||||
<Col className="w-full p-4">
|
||||
<Title className="mb-4">{t('compatibility.title','Your Compatibility Questions')}</Title>
|
||||
<Title className="mb-4">
|
||||
{t('compatibility.title', 'Your Compatibility Questions')}
|
||||
</Title>
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
value={keyword}
|
||||
placeholder={t(
|
||||
'compatibility.search_placeholder',
|
||||
'Search questions and answers...'
|
||||
)}
|
||||
className={'w-full max-w-xs mb-4'}
|
||||
onChange={(e) => {
|
||||
setKeyword(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<UncontrolledTabs
|
||||
trackingName={'compatibility page'}
|
||||
name={'compatibility-page'}
|
||||
tabs={[
|
||||
{
|
||||
title: `${t('compatibility.tabs.answered','Answered')} ${sep}(${answered.length})`,
|
||||
title: `${t('compatibility.tabs.answered', 'Answered')} ${sep}(${answered.length})`,
|
||||
content: (
|
||||
<QuestionList
|
||||
questions={answered}
|
||||
@@ -96,11 +137,12 @@ export default function CompatibilityPage() {
|
||||
isLoading={isLoading}
|
||||
user={user}
|
||||
refreshCompatibilityAll={refreshCompatibilityAll}
|
||||
keyword={keyword}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: `${t('compatibility.tabs.to_answer','To Answer')} ${sep}(${notAnswered.length})`,
|
||||
title: `${t('compatibility.tabs.to_answer', 'To Answer')} ${sep}(${notAnswered.length})`,
|
||||
content: (
|
||||
<QuestionList
|
||||
questions={notAnswered}
|
||||
@@ -108,11 +150,12 @@ export default function CompatibilityPage() {
|
||||
isLoading={isLoading}
|
||||
user={user}
|
||||
refreshCompatibilityAll={refreshCompatibilityAll}
|
||||
keyword={keyword}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: `${t('compatibility.tabs.skipped','Skipped')} ${sep}(${skipped.length})`,
|
||||
title: `${t('compatibility.tabs.skipped', 'Skipped')} ${sep}(${skipped.length})`,
|
||||
content: (
|
||||
<QuestionList
|
||||
questions={skipped}
|
||||
@@ -120,17 +163,23 @@ export default function CompatibilityPage() {
|
||||
isLoading={isLoading}
|
||||
user={user}
|
||||
refreshCompatibilityAll={refreshCompatibilityAll}
|
||||
keyword={keyword}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Col>
|
||||
:
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center">
|
||||
<div className="text-xl">{t('compatibility.sign_in_prompt','Please sign in to view your compatibility questions')}</div>
|
||||
<div className="text-xl">
|
||||
{t(
|
||||
'compatibility.sign_in_prompt',
|
||||
'Please sign in to view your compatibility questions'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</PageBase>
|
||||
)
|
||||
}
|
||||
@@ -141,12 +190,14 @@ function QuestionList({
|
||||
isLoading,
|
||||
user,
|
||||
refreshCompatibilityAll,
|
||||
keyword,
|
||||
}: {
|
||||
questions: QuestionWithAnswer[]
|
||||
status: 'answered' | 'not-answered' | 'skipped'
|
||||
isLoading: boolean
|
||||
user: User
|
||||
refreshCompatibilityAll: () => void
|
||||
keyword: string
|
||||
}) {
|
||||
const t = useT()
|
||||
const BATCH_SIZE = 100
|
||||
@@ -165,18 +216,38 @@ function QuestionList({
|
||||
setVisibleCount((prev) => Math.min(prev + BATCH_SIZE, questions.length))
|
||||
console.log('end loadMore')
|
||||
return true
|
||||
}, [visibleCount, questions.length]);
|
||||
}, [visibleCount, questions.length])
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading && questions.length === 0) {
|
||||
return <CompassLoadingIndicator/>
|
||||
}
|
||||
|
||||
if (questions.length === 0) {
|
||||
if (!isLoading && questions.length === 0) {
|
||||
return (
|
||||
<div className="text-ink-500 p-4">
|
||||
{status === 'answered' && t('compatibility.empty.answered',"You haven't answered any questions yet.")}
|
||||
{status === 'not-answered' && t('compatibility.empty.not_answered',"All questions have been answered!")}
|
||||
{status === 'skipped' && t('compatibility.empty.skipped',"You haven't skipped any questions.")}
|
||||
{keyword ? (
|
||||
t('compatibility.empty.no_results', 'No results for "{keyword}"', {
|
||||
keyword,
|
||||
})
|
||||
) : (
|
||||
<>
|
||||
{status === 'answered' &&
|
||||
t(
|
||||
'compatibility.empty.answered',
|
||||
"You haven't answered any questions yet."
|
||||
)}
|
||||
{status === 'not-answered' &&
|
||||
t(
|
||||
'compatibility.empty.not_answered',
|
||||
'All questions have been answered!'
|
||||
)}
|
||||
{status === 'skipped' &&
|
||||
t(
|
||||
'compatibility.empty.skipped',
|
||||
"You haven't skipped any questions."
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -188,7 +259,10 @@ function QuestionList({
|
||||
{visibleQuestions.map((q) => (
|
||||
<div
|
||||
key={q.id}
|
||||
className="bg-canvas-0 border-canvas-100 rounded-lg border px-2 pt-2 shadow-sm transition-colors"
|
||||
className={clsx(
|
||||
"bg-canvas-0 border-canvas-100 rounded-lg border px-2 pt-2 shadow-sm transition-colors",
|
||||
isLoading && 'animate-pulse opacity-80'
|
||||
)}
|
||||
>
|
||||
<CompatibilityAnswerBlock
|
||||
key={q.answer?.question_id}
|
||||
|
||||
Reference in New Issue
Block a user