From b41c08b5431716a113b4b6efc33a396513e474a2 Mon Sep 17 00:00:00 2001 From: Jeff Sieu Date: Sun, 30 Oct 2022 22:15:38 +0800 Subject: [PATCH] [questions][feat] show location statistics by country --- .../card/question/BaseQuestionCard.tsx | 28 ++++++++-- .../[questionId]/[questionSlug]/index.tsx | 2 +- apps/portal/src/pages/questions/browse.tsx | 47 ++++++++++------ apps/portal/src/pages/questions/lists.tsx | 2 +- .../questions/questions-question-router.ts | 56 +++++++++---------- .../questions-question-user-router.ts | 1 + apps/portal/src/types/questions.d.ts | 6 +- .../questions/relabelQuestionAggregates.ts | 8 +-- 8 files changed, 86 insertions(+), 64 deletions(-) diff --git a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx index d4c534e4..4d8059e6 100644 --- a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx +++ b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { ChatBubbleBottomCenterTextIcon, CheckIcon, @@ -18,6 +18,8 @@ import QuestionAggregateBadge from '../../QuestionAggregateBadge'; import QuestionTypeBadge from '../../QuestionTypeBadge'; import VotingButtons from '../../VotingButtons'; +import type { CountryInfo } from '~/types/questions'; + type UpvoteProps = | { showVoteButtons: true; @@ -51,13 +53,13 @@ type AnswerStatisticsProps = type AggregateStatisticsProps = | { companies: Record; - locations: Record; + countries: Record; roles: Record; showAggregateStatistics: true; } | { companies?: never; - locations?: never; + countries?: never; roles?: never; showAggregateStatistics?: false; }; @@ -136,7 +138,7 @@ export default function BaseQuestionCard({ upvoteCount, timestamp, roles, - locations, + countries, showHover, onReceivedSubmit, showDeleteButton, @@ -147,6 +149,22 @@ export default function BaseQuestionCard({ const [showReceivedForm, setShowReceivedForm] = useState(false); const { handleDownvote, handleUpvote, vote } = useQuestionVote(questionId); const hoverClass = showHover ? 'hover:bg-slate-50' : ''; + + const locations = useMemo(() => { + if (countries === undefined) { + return undefined; + } + + const countryCount: Record = {}; + // Decompose countries + for (const country of Object.keys(countries)) { + const { total } = countries[country]; + countryCount[country] = total; + } + + return countryCount; + }, [countries]); + const cardContent = ( <> {showVoteButtons && ( @@ -168,7 +186,7 @@ export default function BaseQuestionCard({ variant="primary" /> diff --git a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx index 7cfe0533..0b435c3c 100644 --- a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx +++ b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx @@ -192,7 +192,7 @@ export default function QuestionPage() { - selectedCompanies.length > 0 || + selectedCompanySlugs.length > 0 || selectedQuestionTypes.length > 0 || selectedQuestionAge !== 'all' || selectedRoles.length > 0 || selectedLocations.length > 0, [ - selectedCompanies, + selectedCompanySlugs, selectedQuestionTypes, selectedQuestionAge, selectedRoles, @@ -150,15 +153,18 @@ export default function QuestionsBrowsePage() { [ 'questions.questions.getQuestionsByFilter', { - companyNames: selectedCompanies, + // TODO: Enable filtering by cities, companies and states + cityIds: [], + companyIds: selectedCompanySlugs.map((slug) => slug.split('_')[0]), + countryIds: [], endDate: today, limit: 10, - locations: selectedLocations, questionTypes: selectedQuestionTypes, roles: selectedRoles, sortOrder, sortType, startDate, + stateIds: [], }, ], { @@ -235,7 +241,7 @@ export default function QuestionsBrowsePage() { Router.replace({ pathname, query: { - companies: selectedCompanies, + companies: selectedCompanySlugs, locations: selectedLocations, questionAge: selectedQuestionAge, questionTypes: selectedQuestionTypes, @@ -251,7 +257,7 @@ export default function QuestionsBrowsePage() { areSearchOptionsInitialized, loaded, pathname, - selectedCompanies, + selectedCompanySlugs, selectedRoles, selectedLocations, selectedQuestionAge, @@ -261,13 +267,13 @@ export default function QuestionsBrowsePage() { ]); const selectedCompanyOptions = useMemo(() => { - return selectedCompanies.map((company) => ({ + return selectedCompanySlugs.map((company) => ({ checked: true, id: company, label: company, value: company, })); - }, [selectedCompanies]); + }, [selectedCompanySlugs]); const selectedRoleOptions = useMemo(() => { return selectedRoles.map((role) => ({ @@ -301,7 +307,7 @@ export default function QuestionsBrowsePage() { label="Clear filters" variant="tertiary" onClick={() => { - setSelectedCompanies([]); + setSelectedCompanySlugs([]); setSelectedQuestionTypes([]); setSelectedQuestionAge('all'); setSelectedRoles([]); @@ -316,8 +322,8 @@ export default function QuestionsBrowsePage() { {...field} clearOnSelect={true} filterOption={(option) => { - return !selectedCompanies.some((company) => { - return company === option.value; + return !selectedCompanySlugs.some((companySlug) => { + return companySlug === `${option.id}_${option.label}`; }); }} isLabelHidden={true} @@ -333,10 +339,15 @@ export default function QuestionsBrowsePage() { )} onOptionChange={(option) => { if (option.checked) { - setSelectedCompanies([...selectedCompanies, option.label]); + setSelectedCompanySlugs([ + ...selectedCompanySlugs, + `${option.id}_${option.label}`, + ]); } else { - setSelectedCompanies( - selectedCompanies.filter((company) => company !== option.label), + setSelectedCompanySlugs( + selectedCompanySlugs.filter( + (companySlug) => companySlug !== `${option.id}_${option.label}`, + ), ); } }} @@ -471,7 +482,7 @@ export default function QuestionsBrowsePage() { {(questionsQueryData?.pages ?? []).flatMap( ({ data: questions }) => questions.map((question) => { - const { companyCounts, locationCounts, roleCounts } = + const { companyCounts, countryCounts, roleCounts } = relabelQuestionAggregates( question.aggregatedQuestionEncounters, ); @@ -482,10 +493,10 @@ export default function QuestionsBrowsePage() { answerCount={question.numAnswers} companies={companyCounts} content={question.content} + countries={countryCounts} href={`/questions/${question.id}/${createSlug( question.content, )}`} - locations={locationCounts} questionId={question.id} receivedCount={question.receivedCount} roles={roleCounts} diff --git a/apps/portal/src/pages/questions/lists.tsx b/apps/portal/src/pages/questions/lists.tsx index cbf1b276..1c79bd32 100644 --- a/apps/portal/src/pages/questions/lists.tsx +++ b/apps/portal/src/pages/questions/lists.tsx @@ -187,7 +187,7 @@ export default function ListPage() { href={`/questions/${question.id}/${createSlug( question.content, )}`} - locations={locationCounts} + countries={locationCounts} questionId={question.id} receivedCount={question.receivedCount} roles={roleCounts} diff --git a/apps/portal/src/server/router/questions/questions-question-router.ts b/apps/portal/src/server/router/questions/questions-question-router.ts index 83ecb418..c41ef210 100644 --- a/apps/portal/src/server/router/questions/questions-question-router.ts +++ b/apps/portal/src/server/router/questions/questions-question-router.ts @@ -1,8 +1,9 @@ import { z } from 'zod'; import { QuestionsQuestionType } from '@prisma/client'; -import { JobTitleLabels } from '~/components/shared/JobTitles'; import { TRPCError } from '@trpc/server'; +import { JobTitleLabels } from '~/components/shared/JobTitles'; + import { createQuestionWithAggregateData } from '~/utils/questions/server/aggregate-encounters'; import { createRouter } from '../context'; @@ -12,19 +13,18 @@ import { SortOrder, SortType } from '~/types/questions.d'; export const questionsQuestionRouter = createRouter() .query('getQuestionsByFilter', { input: z.object({ + cityIds: z.string().array(), companyIds: z.string().array(), + countryIds: z.string().array(), cursor: z.string().nullish(), endDate: z.date().default(new Date()), limit: z.number().min(1).default(50), - countryIds: z.string().array(), - cityIds: z.string().array(), - stateIds: z.string().array(), - locations: z.string().array(), questionTypes: z.nativeEnum(QuestionsQuestionType).array(), roles: z.nativeEnum(JobTitleLabels).array(), sortOrder: z.nativeEnum(SortOrder), sortType: z.nativeEnum(SortType), startDate: z.date().optional(), + stateIds: z.string().array(), }), async resolve({ ctx, input }) { const { cursor } = input; @@ -59,12 +59,12 @@ export const questionsQuestionRouter = createRouter() }, encounters: { select: { + city: true, company: true, country: true, - city: true, - state: true, role: true, seenAt: true, + state: true, }, }, user: { @@ -99,13 +99,7 @@ export const questionsQuestionRouter = createRouter() }, } : {}), - ...(input.locations.length > 0 - ? { - location: { - in: input.locations, - }, - } - : {}), + // TODO: Add filter for cityIds, countryIds, stateIds ...(input.roles.length > 0 ? { role: { @@ -154,12 +148,12 @@ export const questionsQuestionRouter = createRouter() }, encounters: { select: { + city: true, company: true, country: true, - city: true, - state: true, role: true, seenAt: true, + state: true, }, }, user: { @@ -190,21 +184,23 @@ export const questionsQuestionRouter = createRouter() async resolve({ ctx, input }) { const escapeChars = /[()|&:*!]/g; - const query = - input.content - .replace(escapeChars, " ") - .trim() - .split(/\s+/) - .join(" | "); + const query = input.content + .replace(escapeChars, ' ') + .trim() + .split(/\s+/) + .join(' | '); - const relatedQuestionsId : Array<{id:string}> = await ctx.prisma.$queryRaw` + const relatedQuestionsId: Array<{ id: string }> = 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; `; - const relatedQuestionsIdArray = relatedQuestionsId.map(current => current.id); + const relatedQuestionsIdArray = relatedQuestionsId.map( + (current) => current.id, + ); const relatedQuestionsData = await ctx.prisma.questionsQuestion.findMany({ include: { @@ -216,12 +212,12 @@ export const questionsQuestionRouter = createRouter() }, encounters: { select: { + city: true, company: true, country: true, - city: true, - state: true, role: true, seenAt: true, + state: true, }, }, user: { @@ -232,9 +228,9 @@ export const questionsQuestionRouter = createRouter() votes: true, }, where: { - id : { - in : relatedQuestionsIdArray, - } + id: { + in: relatedQuestionsIdArray, + }, }, }); @@ -243,5 +239,5 @@ export const questionsQuestionRouter = createRouter() ); return processedQuestionsData; - } + }, }); diff --git a/apps/portal/src/server/router/questions/questions-question-user-router.ts b/apps/portal/src/server/router/questions/questions-question-user-router.ts index caf7d409..1a138fbd 100644 --- a/apps/portal/src/server/router/questions/questions-question-user-router.ts +++ b/apps/portal/src/server/router/questions/questions-question-user-router.ts @@ -27,6 +27,7 @@ export const questionsQuestionUserRouter = createProtectedRouter() id: input.companyId, }, }, + // To do: Fix this location: input.location, role: input.role, seenAt: input.seenAt, diff --git a/apps/portal/src/types/questions.d.ts b/apps/portal/src/types/questions.d.ts index 8f64f707..e142db5a 100644 --- a/apps/portal/src/types/questions.d.ts +++ b/apps/portal/src/types/questions.d.ts @@ -15,19 +15,19 @@ export type Question = { }; export type StateInfo = { - total: number; cityCounts: Record; + total: number; }; export type CountryInfo = { - total: number; stateInfos: Record; + total: number; }; export type AggregatedQuestionEncounter = { companyCounts: Record; - latestSeenAt: Date; countryCounts: Record; + latestSeenAt: Date; roleCounts: Record; }; diff --git a/apps/portal/src/utils/questions/relabelQuestionAggregates.ts b/apps/portal/src/utils/questions/relabelQuestionAggregates.ts index 50a2a5dd..05ee68e9 100644 --- a/apps/portal/src/utils/questions/relabelQuestionAggregates.ts +++ b/apps/portal/src/utils/questions/relabelQuestionAggregates.ts @@ -3,10 +3,8 @@ import { JobTitleLabels } from '~/components/shared/JobTitles'; import type { AggregatedQuestionEncounter } from '~/types/questions'; export default function relabelQuestionAggregates({ - locationCounts, - companyCounts, roleCounts, - latestSeenAt, + ...rest }: AggregatedQuestionEncounter) { const newRoleCounts = Object.fromEntries( Object.entries(roleCounts).map(([roleId, count]) => [ @@ -16,10 +14,8 @@ export default function relabelQuestionAggregates({ ); const relabeledAggregate: AggregatedQuestionEncounter = { - companyCounts, - latestSeenAt, - locationCounts, roleCounts: newRoleCounts, + ...rest, }; return relabeledAggregate;