mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 12:43:12 +08:00
[questions][feat] add content search (#478)
Co-authored-by: Jeff Sieu <jeffsy00@gmail.com>
This commit is contained in:
@ -9,10 +9,14 @@ import SortOptionsSelect from './SortOptionsSelect';
|
|||||||
|
|
||||||
export type QuestionSearchBarProps = SortOptionsSelectProps & {
|
export type QuestionSearchBarProps = SortOptionsSelectProps & {
|
||||||
onFilterOptionsToggle: () => void;
|
onFilterOptionsToggle: () => void;
|
||||||
|
onQueryChange: (query: string) => void;
|
||||||
|
query: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function QuestionSearchBar({
|
export default function QuestionSearchBar({
|
||||||
onFilterOptionsToggle,
|
onFilterOptionsToggle,
|
||||||
|
onQueryChange,
|
||||||
|
query,
|
||||||
...sortOptionsSelectProps
|
...sortOptionsSelectProps
|
||||||
}: QuestionSearchBarProps) {
|
}: QuestionSearchBarProps) {
|
||||||
return (
|
return (
|
||||||
@ -24,6 +28,10 @@ export default function QuestionSearchBar({
|
|||||||
placeholder="Search by content"
|
placeholder="Search by content"
|
||||||
startAddOn={MagnifyingGlassIcon}
|
startAddOn={MagnifyingGlassIcon}
|
||||||
startAddOnType="icon"
|
startAddOnType="icon"
|
||||||
|
value={query}
|
||||||
|
onChange={(value) => {
|
||||||
|
onQueryChange(value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end justify-end gap-4">
|
<div className="flex items-end justify-end gap-4">
|
||||||
|
@ -218,7 +218,7 @@ export default function BaseQuestionCard({
|
|||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'whitespace-pre-line',
|
'whitespace-pre-line font-semibold',
|
||||||
truncateContent && 'line-clamp-2 text-ellipsis',
|
truncateContent && 'line-clamp-2 text-ellipsis',
|
||||||
)}>
|
)}>
|
||||||
{content}
|
{content}
|
||||||
|
@ -42,7 +42,9 @@ export default function CreateQuestionEncounterForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p className="font-md text-md text-slate-600">I saw this question at</p>
|
<p className="font-md text-md text-slate-600">
|
||||||
|
I saw this question {step <= 1 ? 'at' : step === 2 ? 'for' : 'on'}
|
||||||
|
</p>
|
||||||
{step === 0 && (
|
{step === 0 && (
|
||||||
<div>
|
<div>
|
||||||
<CompanyTypeahead
|
<CompanyTypeahead
|
||||||
|
@ -37,6 +37,8 @@ import { SortOrder } from '~/types/questions.d';
|
|||||||
export default function QuestionsBrowsePage() {
|
export default function QuestionsBrowsePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
|
||||||
const [
|
const [
|
||||||
selectedCompanySlugs,
|
selectedCompanySlugs,
|
||||||
setSelectedCompanySlugs,
|
setSelectedCompanySlugs,
|
||||||
@ -160,13 +162,14 @@ export default function QuestionsBrowsePage() {
|
|||||||
|
|
||||||
const questionsInfiniteQuery = trpc.useInfiniteQuery(
|
const questionsInfiniteQuery = trpc.useInfiniteQuery(
|
||||||
[
|
[
|
||||||
'questions.questions.getQuestionsByFilter',
|
'questions.questions.getQuestionsByFilterAndContent',
|
||||||
{
|
{
|
||||||
// TODO: Enable filtering by countryIds and stateIds
|
// TODO: Enable filtering by countryIds and stateIds
|
||||||
cityIds: selectedLocations
|
cityIds: selectedLocations
|
||||||
.map(({ cityId }) => cityId)
|
.map(({ cityId }) => cityId)
|
||||||
.filter((id) => id !== undefined) as Array<string>,
|
.filter((id) => id !== undefined) as Array<string>,
|
||||||
companyIds: selectedCompanySlugs.map((slug) => slug.split('_')[0]),
|
companyIds: selectedCompanySlugs.map((slug) => slug.split('_')[0]),
|
||||||
|
content: query,
|
||||||
countryIds: [],
|
countryIds: [],
|
||||||
endDate: today,
|
endDate: today,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
@ -475,8 +478,8 @@ export default function QuestionsBrowsePage() {
|
|||||||
</Head>
|
</Head>
|
||||||
<main className="flex flex-1 flex-col items-stretch">
|
<main className="flex flex-1 flex-col items-stretch">
|
||||||
<div className="flex h-full flex-1">
|
<div className="flex h-full flex-1">
|
||||||
<section className="flex min-h-0 flex-1 flex-col items-center overflow-auto">
|
<section className="min-h-0 flex-1 overflow-auto">
|
||||||
<div className="m-4 flex max-w-3xl flex-1 flex-col items-stretch justify-start gap-6">
|
<div className="my-4 mx-auto flex max-w-3xl flex-col items-stretch justify-start gap-6">
|
||||||
<ContributeQuestionCard
|
<ContributeQuestionCard
|
||||||
onSubmit={(data) => {
|
onSubmit={(data) => {
|
||||||
const { cityId, countryId, stateId } = data.location;
|
const { cityId, countryId, stateId } = data.location;
|
||||||
@ -495,11 +498,15 @@ export default function QuestionsBrowsePage() {
|
|||||||
<div className="flex flex-col items-stretch gap-4">
|
<div className="flex flex-col items-stretch gap-4">
|
||||||
<div className="sticky top-0 border-b border-slate-300 bg-slate-50 py-4">
|
<div className="sticky top-0 border-b border-slate-300 bg-slate-50 py-4">
|
||||||
<QuestionSearchBar
|
<QuestionSearchBar
|
||||||
|
query={query}
|
||||||
sortOrderValue={sortOrder}
|
sortOrderValue={sortOrder}
|
||||||
sortTypeValue={sortType}
|
sortTypeValue={sortType}
|
||||||
onFilterOptionsToggle={() => {
|
onFilterOptionsToggle={() => {
|
||||||
setFilterDrawerOpen(!filterDrawerOpen);
|
setFilterDrawerOpen(!filterDrawerOpen);
|
||||||
}}
|
}}
|
||||||
|
onQueryChange={(newQuery) => {
|
||||||
|
setQuery(newQuery);
|
||||||
|
}}
|
||||||
onSortOrderChange={setSortOrder}
|
onSortOrderChange={setSortOrder}
|
||||||
onSortTypeChange={setSortType}
|
onSortTypeChange={setSortType}
|
||||||
/>
|
/>
|
||||||
|
@ -236,7 +236,8 @@ export const questionsQuestionRouter = createRouter()
|
|||||||
SELECT id FROM "QuestionsQuestion"
|
SELECT id FROM "QuestionsQuestion"
|
||||||
WHERE
|
WHERE
|
||||||
to_tsvector("content") @@ to_tsquery('english', ${query})
|
to_tsvector("content") @@ to_tsquery('english', ${query})
|
||||||
ORDER BY ts_rank_cd(to_tsvector("content"), to_tsquery('english', ${query}), 4) DESC;
|
ORDER BY ts_rank_cd(to_tsvector("content"), to_tsquery('english', ${query}), 4) DESC
|
||||||
|
LIMIT 3;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const relatedQuestionsIdArray = relatedQuestionsId.map(
|
const relatedQuestionsIdArray = relatedQuestionsId.map(
|
||||||
@ -281,4 +282,183 @@ export const questionsQuestionRouter = createRouter()
|
|||||||
|
|
||||||
return processedQuestionsData;
|
return processedQuestionsData;
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
.query('getQuestionsByFilterAndContent', {
|
||||||
|
input: z.object({
|
||||||
|
cityIds: z.string().array(),
|
||||||
|
companyIds: z.string().array(),
|
||||||
|
content: z.string(),
|
||||||
|
countryIds: z.string().array(),
|
||||||
|
cursor: z.string().nullish(),
|
||||||
|
endDate: z.date().default(new Date()),
|
||||||
|
limit: z.number().min(1).default(50),
|
||||||
|
questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
|
||||||
|
roles: z.string().array(),
|
||||||
|
sortOrder: z.nativeEnum(SortOrder),
|
||||||
|
sortType: z.nativeEnum(SortType),
|
||||||
|
startDate: z.date().optional(),
|
||||||
|
stateIds: z.string().array(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
const escapeChars = /[()|&:*!]/g;
|
||||||
|
|
||||||
|
const query = input.content
|
||||||
|
.replace(escapeChars, ' ')
|
||||||
|
.trim()
|
||||||
|
.split(/\s+/)
|
||||||
|
.join(' | ');
|
||||||
|
|
||||||
|
let relatedQuestionsId: Array<{ id: string }> = [];
|
||||||
|
|
||||||
|
if (input.content !== "") {
|
||||||
|
relatedQuestionsId = await ctx.prisma
|
||||||
|
.$queryRaw`
|
||||||
|
SELECT id FROM "QuestionsQuestion"
|
||||||
|
WHERE
|
||||||
|
to_tsvector("content") @@ to_tsquery('english', ${query})
|
||||||
|
ORDER BY ts_rank_cd(to_tsvector("content"), to_tsquery('english', ${query}), 4) DESC
|
||||||
|
LIMIT 3;
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const relatedQuestionsIdArray = relatedQuestionsId.map(
|
||||||
|
(current) => current.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { cursor } = input;
|
||||||
|
|
||||||
|
const sortCondition =
|
||||||
|
input.sortType === SortType.TOP
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
upvotes: input.sortOrder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: input.sortOrder,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
lastSeenAt: input.sortOrder,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: input.sortOrder,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const questionsData = await ctx.prisma.questionsQuestion.findMany({
|
||||||
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
|
include: {
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
answers: true,
|
||||||
|
comments: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
encounters: {
|
||||||
|
select: {
|
||||||
|
city: true,
|
||||||
|
company: true,
|
||||||
|
country: true,
|
||||||
|
role: true,
|
||||||
|
seenAt: true,
|
||||||
|
state: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
votes: true,
|
||||||
|
},
|
||||||
|
orderBy: sortCondition,
|
||||||
|
take: input.limit + 1,
|
||||||
|
where: {
|
||||||
|
id: input.content !== "" ? {
|
||||||
|
in: relatedQuestionsIdArray,
|
||||||
|
} : undefined,
|
||||||
|
...(input.questionTypes.length > 0
|
||||||
|
? {
|
||||||
|
questionType: {
|
||||||
|
in: input.questionTypes,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
encounters: {
|
||||||
|
some: {
|
||||||
|
seenAt: {
|
||||||
|
gte: input.startDate,
|
||||||
|
lte: input.endDate,
|
||||||
|
},
|
||||||
|
...(input.companyIds.length > 0
|
||||||
|
? {
|
||||||
|
company: {
|
||||||
|
id: {
|
||||||
|
in: input.companyIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(input.cityIds.length > 0
|
||||||
|
? {
|
||||||
|
city: {
|
||||||
|
id: {
|
||||||
|
in: input.cityIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(input.countryIds.length > 0
|
||||||
|
? {
|
||||||
|
country: {
|
||||||
|
id: {
|
||||||
|
in: input.countryIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(input.stateIds.length > 0
|
||||||
|
? {
|
||||||
|
state: {
|
||||||
|
id: {
|
||||||
|
in: input.stateIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(input.roles.length > 0
|
||||||
|
? {
|
||||||
|
role: {
|
||||||
|
in: input.roles,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const processedQuestionsData = questionsData.map(
|
||||||
|
createQuestionWithAggregateData,
|
||||||
|
);
|
||||||
|
|
||||||
|
let nextCursor: typeof cursor | undefined = undefined;
|
||||||
|
|
||||||
|
if (questionsData.length > input.limit) {
|
||||||
|
const nextItem = questionsData.pop()!;
|
||||||
|
processedQuestionsData.pop();
|
||||||
|
|
||||||
|
const nextIdCursor: string | undefined = nextItem.id;
|
||||||
|
|
||||||
|
nextCursor = nextIdCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: processedQuestionsData,
|
||||||
|
nextCursor,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user