[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 { 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<string, number>;
locations: Record<string, number>;
countries: Record<string, CountryInfo>;
roles: Record<string, number>;
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<string, number> = {};
// 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"
/>
<QuestionAggregateBadge
statistics={locations}
statistics={locations!}
variant="success"
/>
<QuestionAggregateBadge statistics={roles} variant="danger" />

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

@ -34,8 +34,11 @@ import { SortOrder } from '~/types/questions.d';
export default function QuestionsBrowsePage() {
const router = useRouter();
const [selectedCompanies, setSelectedCompanies, areCompaniesInitialized] =
useSearchParam('companies');
const [
selectedCompanySlugs,
setSelectedCompanySlugs,
areCompaniesInitialized,
] = useSearchParam('companies');
const [
selectedQuestionTypes,
setSelectedQuestionTypes,
@ -121,13 +124,13 @@ export default function QuestionsBrowsePage() {
const hasFilters = useMemo(
() =>
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}

@ -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}

@ -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;
}
},
});

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

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

@ -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;

Loading…
Cancel
Save