From 85d49ad4cd21d1ad2204c45f8175143bf1d1705a Mon Sep 17 00:00:00 2001 From: hpkoh <53825802+hpkoh@users.noreply.github.com> Date: Mon, 10 Oct 2022 13:08:02 +0800 Subject: [PATCH] [questions][feat] add nested create encounter (#339) Co-authored-by: Jeff Sieu --- apps/portal/prisma/schema.prisma | 4 +- .../questions/ContributeQuestionForm.tsx | 23 ++++------ .../components/questions/LandingComponent.tsx | 15 ++++--- .../questions/filter/FilterSection.tsx | 2 +- apps/portal/src/pages/questions/index.tsx | 36 +++++++++++---- .../router/questions-question-router.ts | 45 ++++++++++++++----- apps/portal/src/utils/questions/constants.ts | 24 +++++----- .../src/utils/questions/useSearchFilter.ts | 23 +++++++--- 8 files changed, 113 insertions(+), 59 deletions(-) diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index af2ecc0e..59c53a40 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -204,8 +204,8 @@ model QuestionsQuestionEncounter { userId String? // TODO: sync with models company String @db.Text - location String? @db.Text - role String? @db.Text + location String @db.Text + role String @db.Text seenAt DateTime createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/apps/portal/src/components/questions/ContributeQuestionForm.tsx b/apps/portal/src/components/questions/ContributeQuestionForm.tsx index 4215bae2..367f0d88 100644 --- a/apps/portal/src/components/questions/ContributeQuestionForm.tsx +++ b/apps/portal/src/components/questions/ContributeQuestionForm.tsx @@ -3,17 +3,10 @@ import { useForm } from 'react-hook-form'; import { BuildingOffice2Icon, CalendarDaysIcon, - // UserIcon, + UserIcon, } from '@heroicons/react/24/outline'; import type { QuestionsQuestionType } from '@prisma/client'; -import { - Button, - Collapsible, - Select, - // HorizontalDivider, - TextArea, - TextInput, -} from '@tih/ui'; +import { Button, Collapsible, Select, TextArea, TextInput } from '@tih/ui'; import { QUESTION_TYPES } from '~/utils/questions/constants'; import { @@ -21,7 +14,6 @@ import { useSelectRegister, } from '~/utils/questions/useFormRegister'; -// Import SimilarQuestionCard from './card/SimilarQuestionCard'; import Checkbox from './ui-patch/Checkbox'; export type ContributeQuestionData = { @@ -31,6 +23,7 @@ export type ContributeQuestionData = { position: string; questionContent: string; questionType: QuestionsQuestionType; + role: string; }; export type ContributeQuestionFormProps = { @@ -99,19 +92,21 @@ export default function ContributeQuestionForm({
- {/*
+
-
*/} +
{/*
diff --git a/apps/portal/src/components/questions/LandingComponent.tsx b/apps/portal/src/components/questions/LandingComponent.tsx index 98614794..7f3ffee4 100644 --- a/apps/portal/src/components/questions/LandingComponent.tsx +++ b/apps/portal/src/components/questions/LandingComponent.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { ArrowSmallRightIcon } from '@heroicons/react/24/outline'; +import type { QuestionsQuestionType } from '@prisma/client'; import { Button, Select } from '@tih/ui'; import { @@ -11,7 +12,7 @@ import { export type LandingQueryData = { company: string; location: string; - questionType: string; + questionType: QuestionsQuestionType; }; export type LandingComponentProps = { @@ -22,9 +23,9 @@ export default function LandingComponent({ onLanded: handleLandingQuery, }: LandingComponentProps) { const [landingQueryData, setLandingQueryData] = useState({ - company: 'google', - location: 'singapore', - questionType: 'coding', + company: 'Google', + location: 'Singapore', + questionType: 'CODING', }); const handleChangeCompany = (company: string) => { @@ -35,7 +36,7 @@ export default function LandingComponent({ setLandingQueryData((prev) => ({ ...prev, location })); }; - const handleChangeType = (questionType: string) => { + const handleChangeType = (questionType: QuestionsQuestionType) => { setLandingQueryData((prev) => ({ ...prev, questionType })); }; @@ -61,7 +62,9 @@ export default function LandingComponent({ label="Type" options={QUESTION_TYPES} value={landingQueryData.questionType} - onChange={handleChangeType} + onChange={(value) => { + handleChangeType(value.toUpperCase() as QuestionsQuestionType); + }} />

questions from

diff --git a/apps/portal/src/components/questions/filter/FilterSection.tsx b/apps/portal/src/components/questions/filter/FilterSection.tsx index f7b4f800..cfb6ba5c 100644 --- a/apps/portal/src/components/questions/filter/FilterSection.tsx +++ b/apps/portal/src/components/questions/filter/FilterSection.tsx @@ -10,7 +10,7 @@ export type FilterOption = { value: V; }; -export type FilterChoices = Array< +export type FilterChoices = ReadonlyArray< Omit, 'checked'> >; diff --git a/apps/portal/src/pages/questions/index.tsx b/apps/portal/src/pages/questions/index.tsx index 331ef57b..826a6204 100644 --- a/apps/portal/src/pages/questions/index.tsx +++ b/apps/portal/src/pages/questions/index.tsx @@ -1,3 +1,4 @@ +import { subMonths, subYears } from 'date-fns'; import { useRouter } from 'next/router'; import { useEffect, useMemo, useState } from 'react'; import type { QuestionsQuestionType } from '@prisma/client'; @@ -9,6 +10,7 @@ import type { LandingQueryData } from '~/components/questions/LandingComponent'; import LandingComponent from '~/components/questions/LandingComponent'; import QuestionSearchBar from '~/components/questions/QuestionSearchBar'; +import type { QuestionAge } from '~/utils/questions/constants'; import { COMPANIES, LOCATIONS, @@ -31,24 +33,41 @@ export default function QuestionsHomePage() { selectedQuestionTypes, setSelectedQuestionTypes, areQuestionTypesInitialized, - ] = useSearchFilter('questionTypes'); + ] = useSearchFilter('questionTypes', { + queryParamToValue: (param) => { + return param.toUpperCase() as QuestionsQuestionType; + }, + }); const [ selectedQuestionAge, setSelectedQuestionAge, isQuestionAgeInitialized, - ] = useSearchFilterSingle('questionAge', 'all'); + ] = useSearchFilterSingle('questionAge', { + defaultValue: 'all', + }); const [selectedLocations, setSelectedLocations, areLocationsInitialized] = useSearchFilter('locations'); - // TODO: Implement filtering + const today = useMemo(() => new Date(), []); + const startDate = useMemo(() => { + return selectedQuestionAge === 'last-year' + ? subYears(new Date(), 1) + : selectedQuestionAge === 'last-6-months' + ? subMonths(new Date(), 6) + : selectedQuestionAge === 'last-month' + ? subMonths(new Date(), 1) + : undefined; + }, [selectedQuestionAge]); + const { data: questions } = trpc.useQuery([ 'questions.questions.getQuestionsByFilter', { - // TODO: Update when query accepts multiple question types - questionType: - selectedQuestionTypes.length > 0 - ? (selectedQuestionTypes[0].toUpperCase() as QuestionsQuestionType) - : 'CODING', + companies: selectedCompanies, + endDate: today, + locations: selectedLocations, + questionTypes: selectedQuestionTypes, + roles: [], + startDate, }, ]); @@ -214,6 +233,7 @@ export default function QuestionsHomePage() { content: data.questionContent, location: data.location, questionType: data.questionType, + role: data.role, seenAt: data.date, }); }} diff --git a/apps/portal/src/server/router/questions-question-router.ts b/apps/portal/src/server/router/questions-question-router.ts index bd65474a..39dafd98 100644 --- a/apps/portal/src/server/router/questions-question-router.ts +++ b/apps/portal/src/server/router/questions-question-router.ts @@ -9,10 +9,12 @@ import type { Question } from '~/types/questions'; export const questionsQuestionRouter = createProtectedRouter() .query('getQuestionsByFilter', { input: z.object({ - company: z.string().optional(), - location: z.string().optional(), - questionType: z.nativeEnum(QuestionsQuestionType), - role: z.string().optional(), + companies: z.string().array(), + endDate: z.date(), + locations: z.string().array(), + questionTypes: z.nativeEnum(QuestionsQuestionType).array(), + roles: z.string().array(), + startDate: z.date().optional(), }), async resolve({ ctx, input }) { const questionsData = await ctx.prisma.questionsQuestion.findMany({ @@ -42,7 +44,13 @@ export const questionsQuestionRouter = createProtectedRouter() createdAt: 'desc', }, where: { - questionType: input.questionType, + ...(input.questionTypes.length > 0 + ? { + questionType: { + in: input.questionTypes, + }, + } + : {}), }, }); return questionsData @@ -50,11 +58,17 @@ export const questionsQuestionRouter = createProtectedRouter() for (let i = 0; i < data.encounters.length; i++) { const encounter = data.encounters[i]; const matchCompany = - !input.company || encounter.company === input.company; + input.companies.length === 0 || + input.companies.includes(encounter.company); const matchLocation = - !input.location || encounter.location === input.location; - const matchRole = !input.company || encounter.role === input.role; - if (matchCompany && matchLocation && matchRole) { + input.locations.length === 0 || + input.locations.includes(encounter.location); + const matchRole = + input.roles.length === 0 || input.roles.includes(encounter.role); + const matchDate = + (!input.startDate || encounter.seenAt >= input.startDate) && + encounter.seenAt <= input.endDate; + if (matchCompany && matchLocation && matchRole && matchDate) { return true; } } @@ -174,7 +188,7 @@ export const questionsQuestionRouter = createProtectedRouter() content: z.string(), location: z.string(), questionType: z.nativeEnum(QuestionsQuestionType), - role: z.string().optional(), + role: z.string(), seenAt: z.date(), }), async resolve({ ctx, input }) { @@ -183,6 +197,17 @@ export const questionsQuestionRouter = createProtectedRouter() const question = await ctx.prisma.questionsQuestion.create({ data: { content: input.content, + encounters: { + create: [ + { + company: input.company, + location: input.location, + role: input.role, + seenAt: input.seenAt, + userId, + }, + ], + }, questionType: input.questionType, userId, }, diff --git a/apps/portal/src/utils/questions/constants.ts b/apps/portal/src/utils/questions/constants.ts index 155359d5..357b6f55 100644 --- a/apps/portal/src/utils/questions/constants.ts +++ b/apps/portal/src/utils/questions/constants.ts @@ -5,13 +5,13 @@ import type { FilterChoices } from '~/components/questions/filter/FilterSection' export const COMPANIES: FilterChoices = [ { label: 'Google', - value: 'google', + value: 'Google', }, { label: 'Meta', - value: 'meta', + value: 'Meta', }, -]; +] as const; // Code, design, behavioral export const QUESTION_TYPES: FilterChoices = [ @@ -27,9 +27,11 @@ export const QUESTION_TYPES: FilterChoices = [ label: 'Behavioral', value: 'BEHAVIORAL', }, -]; +] as const; -export const QUESTION_AGES: FilterChoices = [ +export type QuestionAge = 'all' | 'last-6-months' | 'last-month' | 'last-year'; + +export const QUESTION_AGES: FilterChoices = [ { label: 'Last month', value: 'last-month', @@ -46,16 +48,16 @@ export const QUESTION_AGES: FilterChoices = [ label: 'All', value: 'all', }, -]; +] as const; export const LOCATIONS: FilterChoices = [ { label: 'Singapore', - value: 'singapore', + value: 'Singapore', }, { label: 'Menlo Park', - value: 'menlopark', + value: 'Menlo Park', }, { label: 'California', @@ -63,13 +65,13 @@ export const LOCATIONS: FilterChoices = [ }, { label: 'Hong Kong', - value: 'hongkong', + value: 'Hong Kong', }, { label: 'Taiwan', - value: 'taiwan', + value: 'Taiwan', }, -]; +] as const; export const SAMPLE_QUESTION = { answerCount: 10, diff --git a/apps/portal/src/utils/questions/useSearchFilter.ts b/apps/portal/src/utils/questions/useSearchFilter.ts index 392a69c8..1a7bd199 100644 --- a/apps/portal/src/utils/questions/useSearchFilter.ts +++ b/apps/portal/src/utils/questions/useSearchFilter.ts @@ -3,8 +3,12 @@ import { useCallback, useEffect, useState } from 'react'; export const useSearchFilter = ( name: string, - defaultValues?: Array, + opts: { + defaultValues?: Array; + queryParamToValue?: (param: string) => Value; + } = {}, ) => { + const { defaultValues, queryParamToValue = (param) => param } = opts; const [isInitialized, setIsInitialized] = useState(false); const router = useRouter(); @@ -16,7 +20,7 @@ export const useSearchFilter = ( const query = router.query[name]; if (query) { const queryValues = Array.isArray(query) ? query : [query]; - setFilters(queryValues as Array); + setFilters(queryValues.map(queryParamToValue) as Array); } else { // Try to load from local storage const localStorageValue = localStorage.getItem(name); @@ -34,7 +38,7 @@ export const useSearchFilter = ( } setIsInitialized(true); } - }, [isInitialized, name, router]); + }, [isInitialized, name, queryParamToValue, router]); const setFiltersCallback = useCallback( (newFilters: Array) => { @@ -56,11 +60,16 @@ export const useSearchFilter = ( export const useSearchFilterSingle = ( name: string, - defaultValue: Value, + opts: { + defaultValue?: Value; + queryParamToValue?: (param: string) => Value; + } = {}, ) => { - const [filters, setFilters, isInitialized] = useSearchFilter(name, [ - defaultValue, - ]); + const { defaultValue, queryParamToValue } = opts; + const [filters, setFilters, isInitialized] = useSearchFilter(name, { + defaultValues: defaultValue !== undefined ? [defaultValue] : undefined, + queryParamToValue, + }); return [ filters[0],