[questions][feat] show location statistics by country

pull/457/head
Jeff Sieu 3 years ago
parent 97df92292a
commit b41c08b543

@ -1,5 +1,5 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { useState } from 'react'; import { useMemo, useState } from 'react';
import { import {
ChatBubbleBottomCenterTextIcon, ChatBubbleBottomCenterTextIcon,
CheckIcon, CheckIcon,
@ -18,6 +18,8 @@ import QuestionAggregateBadge from '../../QuestionAggregateBadge';
import QuestionTypeBadge from '../../QuestionTypeBadge'; import QuestionTypeBadge from '../../QuestionTypeBadge';
import VotingButtons from '../../VotingButtons'; import VotingButtons from '../../VotingButtons';
import type { CountryInfo } from '~/types/questions';
type UpvoteProps = type UpvoteProps =
| { | {
showVoteButtons: true; showVoteButtons: true;
@ -51,13 +53,13 @@ type AnswerStatisticsProps =
type AggregateStatisticsProps = type AggregateStatisticsProps =
| { | {
companies: Record<string, number>; companies: Record<string, number>;
locations: Record<string, number>; countries: Record<string, CountryInfo>;
roles: Record<string, number>; roles: Record<string, number>;
showAggregateStatistics: true; showAggregateStatistics: true;
} }
| { | {
companies?: never; companies?: never;
locations?: never; countries?: never;
roles?: never; roles?: never;
showAggregateStatistics?: false; showAggregateStatistics?: false;
}; };
@ -136,7 +138,7 @@ export default function BaseQuestionCard({
upvoteCount, upvoteCount,
timestamp, timestamp,
roles, roles,
locations, countries,
showHover, showHover,
onReceivedSubmit, onReceivedSubmit,
showDeleteButton, showDeleteButton,
@ -147,6 +149,22 @@ export default function BaseQuestionCard({
const [showReceivedForm, setShowReceivedForm] = useState(false); const [showReceivedForm, setShowReceivedForm] = useState(false);
const { handleDownvote, handleUpvote, vote } = useQuestionVote(questionId); const { handleDownvote, handleUpvote, vote } = useQuestionVote(questionId);
const hoverClass = showHover ? 'hover:bg-slate-50' : ''; const hoverClass = showHover ? 'hover:bg-slate-50' : '';
const locations = useMemo(() => {
if (countries === undefined) {
return undefined;
}
const countryCount: Record<string, number> = {};
// Decompose countries
for (const country of Object.keys(countries)) {
const { total } = countries[country];
countryCount[country] = total;
}
return countryCount;
}, [countries]);
const cardContent = ( const cardContent = (
<> <>
{showVoteButtons && ( {showVoteButtons && (
@ -168,7 +186,7 @@ export default function BaseQuestionCard({
variant="primary" variant="primary"
/> />
<QuestionAggregateBadge <QuestionAggregateBadge
statistics={locations} statistics={locations!}
variant="success" variant="success"
/> />
<QuestionAggregateBadge statistics={roles} variant="danger" /> <QuestionAggregateBadge statistics={roles} variant="danger" />

@ -192,7 +192,7 @@ export default function QuestionPage() {
<FullQuestionCard <FullQuestionCard
{...question} {...question}
companies={relabeledAggregatedEncounters?.companyCounts ?? {}} companies={relabeledAggregatedEncounters?.companyCounts ?? {}}
locations={relabeledAggregatedEncounters?.locationCounts ?? {}} countries={relabeledAggregatedEncounters?.countryCounts ?? {}}
questionId={question.id} questionId={question.id}
receivedCount={undefined} receivedCount={undefined}
roles={relabeledAggregatedEncounters?.roleCounts ?? {}} roles={relabeledAggregatedEncounters?.roleCounts ?? {}}

@ -34,8 +34,11 @@ import { SortOrder } from '~/types/questions.d';
export default function QuestionsBrowsePage() { export default function QuestionsBrowsePage() {
const router = useRouter(); const router = useRouter();
const [selectedCompanies, setSelectedCompanies, areCompaniesInitialized] = const [
useSearchParam('companies'); selectedCompanySlugs,
setSelectedCompanySlugs,
areCompaniesInitialized,
] = useSearchParam('companies');
const [ const [
selectedQuestionTypes, selectedQuestionTypes,
setSelectedQuestionTypes, setSelectedQuestionTypes,
@ -121,13 +124,13 @@ export default function QuestionsBrowsePage() {
const hasFilters = useMemo( const hasFilters = useMemo(
() => () =>
selectedCompanies.length > 0 || selectedCompanySlugs.length > 0 ||
selectedQuestionTypes.length > 0 || selectedQuestionTypes.length > 0 ||
selectedQuestionAge !== 'all' || selectedQuestionAge !== 'all' ||
selectedRoles.length > 0 || selectedRoles.length > 0 ||
selectedLocations.length > 0, selectedLocations.length > 0,
[ [
selectedCompanies, selectedCompanySlugs,
selectedQuestionTypes, selectedQuestionTypes,
selectedQuestionAge, selectedQuestionAge,
selectedRoles, selectedRoles,
@ -150,15 +153,18 @@ export default function QuestionsBrowsePage() {
[ [
'questions.questions.getQuestionsByFilter', 'questions.questions.getQuestionsByFilter',
{ {
companyNames: selectedCompanies, // TODO: Enable filtering by cities, companies and states
cityIds: [],
companyIds: selectedCompanySlugs.map((slug) => slug.split('_')[0]),
countryIds: [],
endDate: today, endDate: today,
limit: 10, limit: 10,
locations: selectedLocations,
questionTypes: selectedQuestionTypes, questionTypes: selectedQuestionTypes,
roles: selectedRoles, roles: selectedRoles,
sortOrder, sortOrder,
sortType, sortType,
startDate, startDate,
stateIds: [],
}, },
], ],
{ {
@ -235,7 +241,7 @@ export default function QuestionsBrowsePage() {
Router.replace({ Router.replace({
pathname, pathname,
query: { query: {
companies: selectedCompanies, companies: selectedCompanySlugs,
locations: selectedLocations, locations: selectedLocations,
questionAge: selectedQuestionAge, questionAge: selectedQuestionAge,
questionTypes: selectedQuestionTypes, questionTypes: selectedQuestionTypes,
@ -251,7 +257,7 @@ export default function QuestionsBrowsePage() {
areSearchOptionsInitialized, areSearchOptionsInitialized,
loaded, loaded,
pathname, pathname,
selectedCompanies, selectedCompanySlugs,
selectedRoles, selectedRoles,
selectedLocations, selectedLocations,
selectedQuestionAge, selectedQuestionAge,
@ -261,13 +267,13 @@ export default function QuestionsBrowsePage() {
]); ]);
const selectedCompanyOptions = useMemo(() => { const selectedCompanyOptions = useMemo(() => {
return selectedCompanies.map((company) => ({ return selectedCompanySlugs.map((company) => ({
checked: true, checked: true,
id: company, id: company,
label: company, label: company,
value: company, value: company,
})); }));
}, [selectedCompanies]); }, [selectedCompanySlugs]);
const selectedRoleOptions = useMemo(() => { const selectedRoleOptions = useMemo(() => {
return selectedRoles.map((role) => ({ return selectedRoles.map((role) => ({
@ -301,7 +307,7 @@ export default function QuestionsBrowsePage() {
label="Clear filters" label="Clear filters"
variant="tertiary" variant="tertiary"
onClick={() => { onClick={() => {
setSelectedCompanies([]); setSelectedCompanySlugs([]);
setSelectedQuestionTypes([]); setSelectedQuestionTypes([]);
setSelectedQuestionAge('all'); setSelectedQuestionAge('all');
setSelectedRoles([]); setSelectedRoles([]);
@ -316,8 +322,8 @@ export default function QuestionsBrowsePage() {
{...field} {...field}
clearOnSelect={true} clearOnSelect={true}
filterOption={(option) => { filterOption={(option) => {
return !selectedCompanies.some((company) => { return !selectedCompanySlugs.some((companySlug) => {
return company === option.value; return companySlug === `${option.id}_${option.label}`;
}); });
}} }}
isLabelHidden={true} isLabelHidden={true}
@ -333,10 +339,15 @@ export default function QuestionsBrowsePage() {
)} )}
onOptionChange={(option) => { onOptionChange={(option) => {
if (option.checked) { if (option.checked) {
setSelectedCompanies([...selectedCompanies, option.label]); setSelectedCompanySlugs([
...selectedCompanySlugs,
`${option.id}_${option.label}`,
]);
} else { } else {
setSelectedCompanies( setSelectedCompanySlugs(
selectedCompanies.filter((company) => company !== option.label), selectedCompanySlugs.filter(
(companySlug) => companySlug !== `${option.id}_${option.label}`,
),
); );
} }
}} }}
@ -471,7 +482,7 @@ export default function QuestionsBrowsePage() {
{(questionsQueryData?.pages ?? []).flatMap( {(questionsQueryData?.pages ?? []).flatMap(
({ data: questions }) => ({ data: questions }) =>
questions.map((question) => { questions.map((question) => {
const { companyCounts, locationCounts, roleCounts } = const { companyCounts, countryCounts, roleCounts } =
relabelQuestionAggregates( relabelQuestionAggregates(
question.aggregatedQuestionEncounters, question.aggregatedQuestionEncounters,
); );
@ -482,10 +493,10 @@ export default function QuestionsBrowsePage() {
answerCount={question.numAnswers} answerCount={question.numAnswers}
companies={companyCounts} companies={companyCounts}
content={question.content} content={question.content}
countries={countryCounts}
href={`/questions/${question.id}/${createSlug( href={`/questions/${question.id}/${createSlug(
question.content, question.content,
)}`} )}`}
locations={locationCounts}
questionId={question.id} questionId={question.id}
receivedCount={question.receivedCount} receivedCount={question.receivedCount}
roles={roleCounts} roles={roleCounts}

@ -187,7 +187,7 @@ export default function ListPage() {
href={`/questions/${question.id}/${createSlug( href={`/questions/${question.id}/${createSlug(
question.content, question.content,
)}`} )}`}
locations={locationCounts} countries={locationCounts}
questionId={question.id} questionId={question.id}
receivedCount={question.receivedCount} receivedCount={question.receivedCount}
roles={roleCounts} roles={roleCounts}

@ -1,8 +1,9 @@
import { z } from 'zod'; import { z } from 'zod';
import { QuestionsQuestionType } from '@prisma/client'; import { QuestionsQuestionType } from '@prisma/client';
import { JobTitleLabels } from '~/components/shared/JobTitles';
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
import { JobTitleLabels } from '~/components/shared/JobTitles';
import { createQuestionWithAggregateData } from '~/utils/questions/server/aggregate-encounters'; import { createQuestionWithAggregateData } from '~/utils/questions/server/aggregate-encounters';
import { createRouter } from '../context'; import { createRouter } from '../context';
@ -12,19 +13,18 @@ import { SortOrder, SortType } from '~/types/questions.d';
export const questionsQuestionRouter = createRouter() export const questionsQuestionRouter = createRouter()
.query('getQuestionsByFilter', { .query('getQuestionsByFilter', {
input: z.object({ input: z.object({
cityIds: z.string().array(),
companyIds: z.string().array(), companyIds: z.string().array(),
countryIds: z.string().array(),
cursor: z.string().nullish(), cursor: z.string().nullish(),
endDate: z.date().default(new Date()), endDate: z.date().default(new Date()),
limit: z.number().min(1).default(50), 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(), questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
roles: z.nativeEnum(JobTitleLabels).array(), roles: z.nativeEnum(JobTitleLabels).array(),
sortOrder: z.nativeEnum(SortOrder), sortOrder: z.nativeEnum(SortOrder),
sortType: z.nativeEnum(SortType), sortType: z.nativeEnum(SortType),
startDate: z.date().optional(), startDate: z.date().optional(),
stateIds: z.string().array(),
}), }),
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const { cursor } = input; const { cursor } = input;
@ -59,12 +59,12 @@ export const questionsQuestionRouter = createRouter()
}, },
encounters: { encounters: {
select: { select: {
city: true,
company: true, company: true,
country: true, country: true,
city: true,
state: true,
role: true, role: true,
seenAt: true, seenAt: true,
state: true,
}, },
}, },
user: { user: {
@ -99,13 +99,7 @@ export const questionsQuestionRouter = createRouter()
}, },
} }
: {}), : {}),
...(input.locations.length > 0 // TODO: Add filter for cityIds, countryIds, stateIds
? {
location: {
in: input.locations,
},
}
: {}),
...(input.roles.length > 0 ...(input.roles.length > 0
? { ? {
role: { role: {
@ -154,12 +148,12 @@ export const questionsQuestionRouter = createRouter()
}, },
encounters: { encounters: {
select: { select: {
city: true,
company: true, company: true,
country: true, country: true,
city: true,
state: true,
role: true, role: true,
seenAt: true, seenAt: true,
state: true,
}, },
}, },
user: { user: {
@ -190,21 +184,23 @@ export const questionsQuestionRouter = createRouter()
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const escapeChars = /[()|&:*!]/g; const escapeChars = /[()|&:*!]/g;
const query = const query = input.content
input.content .replace(escapeChars, ' ')
.replace(escapeChars, " ") .trim()
.trim() .split(/\s+/)
.split(/\s+/) .join(' | ');
.join(" | ");
const relatedQuestionsId : Array<{id:string}> = await ctx.prisma.$queryRaw` const relatedQuestionsId: Array<{ id: string }> = await ctx.prisma
.$queryRaw`
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;
`; `;
const relatedQuestionsIdArray = relatedQuestionsId.map(current => current.id); const relatedQuestionsIdArray = relatedQuestionsId.map(
(current) => current.id,
);
const relatedQuestionsData = await ctx.prisma.questionsQuestion.findMany({ const relatedQuestionsData = await ctx.prisma.questionsQuestion.findMany({
include: { include: {
@ -216,12 +212,12 @@ export const questionsQuestionRouter = createRouter()
}, },
encounters: { encounters: {
select: { select: {
city: true,
company: true, company: true,
country: true, country: true,
city: true,
state: true,
role: true, role: true,
seenAt: true, seenAt: true,
state: true,
}, },
}, },
user: { user: {
@ -232,9 +228,9 @@ export const questionsQuestionRouter = createRouter()
votes: true, votes: true,
}, },
where: { where: {
id : { id: {
in : relatedQuestionsIdArray, in: relatedQuestionsIdArray,
} },
}, },
}); });
@ -243,5 +239,5 @@ export const questionsQuestionRouter = createRouter()
); );
return processedQuestionsData; return processedQuestionsData;
} },
}); });

@ -27,6 +27,7 @@ export const questionsQuestionUserRouter = createProtectedRouter()
id: input.companyId, id: input.companyId,
}, },
}, },
// To do: Fix this
location: input.location, location: input.location,
role: input.role, role: input.role,
seenAt: input.seenAt, seenAt: input.seenAt,

@ -15,19 +15,19 @@ export type Question = {
}; };
export type StateInfo = { export type StateInfo = {
total: number;
cityCounts: Record<string, number>; cityCounts: Record<string, number>;
total: number;
}; };
export type CountryInfo = { export type CountryInfo = {
total: number;
stateInfos: Record<string, StateInfo>; stateInfos: Record<string, StateInfo>;
total: number;
}; };
export type AggregatedQuestionEncounter = { export type AggregatedQuestionEncounter = {
companyCounts: Record<string, number>; companyCounts: Record<string, number>;
latestSeenAt: Date;
countryCounts: Record<string, CountryInfo>; countryCounts: Record<string, CountryInfo>;
latestSeenAt: Date;
roleCounts: Record<string, number>; roleCounts: Record<string, number>;
}; };

@ -3,10 +3,8 @@ import { JobTitleLabels } from '~/components/shared/JobTitles';
import type { AggregatedQuestionEncounter } from '~/types/questions'; import type { AggregatedQuestionEncounter } from '~/types/questions';
export default function relabelQuestionAggregates({ export default function relabelQuestionAggregates({
locationCounts,
companyCounts,
roleCounts, roleCounts,
latestSeenAt, ...rest
}: AggregatedQuestionEncounter) { }: AggregatedQuestionEncounter) {
const newRoleCounts = Object.fromEntries( const newRoleCounts = Object.fromEntries(
Object.entries(roleCounts).map(([roleId, count]) => [ Object.entries(roleCounts).map(([roleId, count]) => [
@ -16,10 +14,8 @@ export default function relabelQuestionAggregates({
); );
const relabeledAggregate: AggregatedQuestionEncounter = { const relabeledAggregate: AggregatedQuestionEncounter = {
companyCounts,
latestSeenAt,
locationCounts,
roleCounts: newRoleCounts, roleCounts: newRoleCounts,
...rest,
}; };
return relabeledAggregate; return relabeledAggregate;

Loading…
Cancel
Save