[questions][feat] add nested create encounter (#339)

Co-authored-by: Jeff Sieu <jeffsy00@gmail.com>
This commit is contained in:
hpkoh
2022-10-10 13:08:02 +08:00
committed by GitHub
parent 2b68ea7c6a
commit 85d49ad4cd
8 changed files with 113 additions and 59 deletions

View File

@ -204,8 +204,8 @@ model QuestionsQuestionEncounter {
userId String? userId String?
// TODO: sync with models // TODO: sync with models
company String @db.Text company String @db.Text
location String? @db.Text location String @db.Text
role String? @db.Text role String @db.Text
seenAt DateTime seenAt DateTime
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View File

@ -3,17 +3,10 @@ import { useForm } from 'react-hook-form';
import { import {
BuildingOffice2Icon, BuildingOffice2Icon,
CalendarDaysIcon, CalendarDaysIcon,
// UserIcon, UserIcon,
} from '@heroicons/react/24/outline'; } from '@heroicons/react/24/outline';
import type { QuestionsQuestionType } from '@prisma/client'; import type { QuestionsQuestionType } from '@prisma/client';
import { import { Button, Collapsible, Select, TextArea, TextInput } from '@tih/ui';
Button,
Collapsible,
Select,
// HorizontalDivider,
TextArea,
TextInput,
} from '@tih/ui';
import { QUESTION_TYPES } from '~/utils/questions/constants'; import { QUESTION_TYPES } from '~/utils/questions/constants';
import { import {
@ -21,7 +14,6 @@ import {
useSelectRegister, useSelectRegister,
} from '~/utils/questions/useFormRegister'; } from '~/utils/questions/useFormRegister';
// Import SimilarQuestionCard from './card/SimilarQuestionCard';
import Checkbox from './ui-patch/Checkbox'; import Checkbox from './ui-patch/Checkbox';
export type ContributeQuestionData = { export type ContributeQuestionData = {
@ -31,6 +23,7 @@ export type ContributeQuestionData = {
position: string; position: string;
questionContent: string; questionContent: string;
questionType: QuestionsQuestionType; questionType: QuestionsQuestionType;
role: string;
}; };
export type ContributeQuestionFormProps = { export type ContributeQuestionFormProps = {
@ -99,19 +92,21 @@ export default function ContributeQuestionForm({
<div className="min-w-[150px] max-w-[300px] flex-1"> <div className="min-w-[150px] max-w-[300px] flex-1">
<TextInput <TextInput
label="Location" label="Location"
required={true}
startAddOn={CalendarDaysIcon} startAddOn={CalendarDaysIcon}
startAddOnType="icon" startAddOnType="icon"
{...register('location')} {...register('location')}
/> />
</div> </div>
{/* <div className="min-w-[150px] max-w-[200px] flex-1"> <div className="min-w-[150px] max-w-[200px] flex-1">
<TextInput <TextInput
label="Position <TODO>" label="Role"
required={true}
startAddOn={UserIcon} startAddOn={UserIcon}
startAddOnType="icon" startAddOnType="icon"
{...register('position')} {...register('role')}
/> />
</div> */} </div>
</div> </div>
</Collapsible> </Collapsible>
{/* <div className="w-full"> {/* <div className="w-full">

View File

@ -1,5 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { ArrowSmallRightIcon } from '@heroicons/react/24/outline'; import { ArrowSmallRightIcon } from '@heroicons/react/24/outline';
import type { QuestionsQuestionType } from '@prisma/client';
import { Button, Select } from '@tih/ui'; import { Button, Select } from '@tih/ui';
import { import {
@ -11,7 +12,7 @@ import {
export type LandingQueryData = { export type LandingQueryData = {
company: string; company: string;
location: string; location: string;
questionType: string; questionType: QuestionsQuestionType;
}; };
export type LandingComponentProps = { export type LandingComponentProps = {
@ -22,9 +23,9 @@ export default function LandingComponent({
onLanded: handleLandingQuery, onLanded: handleLandingQuery,
}: LandingComponentProps) { }: LandingComponentProps) {
const [landingQueryData, setLandingQueryData] = useState<LandingQueryData>({ const [landingQueryData, setLandingQueryData] = useState<LandingQueryData>({
company: 'google', company: 'Google',
location: 'singapore', location: 'Singapore',
questionType: 'coding', questionType: 'CODING',
}); });
const handleChangeCompany = (company: string) => { const handleChangeCompany = (company: string) => {
@ -35,7 +36,7 @@ export default function LandingComponent({
setLandingQueryData((prev) => ({ ...prev, location })); setLandingQueryData((prev) => ({ ...prev, location }));
}; };
const handleChangeType = (questionType: string) => { const handleChangeType = (questionType: QuestionsQuestionType) => {
setLandingQueryData((prev) => ({ ...prev, questionType })); setLandingQueryData((prev) => ({ ...prev, questionType }));
}; };
@ -61,7 +62,9 @@ export default function LandingComponent({
label="Type" label="Type"
options={QUESTION_TYPES} options={QUESTION_TYPES}
value={landingQueryData.questionType} value={landingQueryData.questionType}
onChange={handleChangeType} onChange={(value) => {
handleChangeType(value.toUpperCase() as QuestionsQuestionType);
}}
/> />
</div> </div>
<p>questions from</p> <p>questions from</p>

View File

@ -10,7 +10,7 @@ export type FilterOption<V extends string = string> = {
value: V; value: V;
}; };
export type FilterChoices<V extends string = string> = Array< export type FilterChoices<V extends string = string> = ReadonlyArray<
Omit<FilterOption<V>, 'checked'> Omit<FilterOption<V>, 'checked'>
>; >;

View File

@ -1,3 +1,4 @@
import { subMonths, subYears } from 'date-fns';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import type { QuestionsQuestionType } from '@prisma/client'; import type { QuestionsQuestionType } from '@prisma/client';
@ -9,6 +10,7 @@ import type { LandingQueryData } from '~/components/questions/LandingComponent';
import LandingComponent from '~/components/questions/LandingComponent'; import LandingComponent from '~/components/questions/LandingComponent';
import QuestionSearchBar from '~/components/questions/QuestionSearchBar'; import QuestionSearchBar from '~/components/questions/QuestionSearchBar';
import type { QuestionAge } from '~/utils/questions/constants';
import { import {
COMPANIES, COMPANIES,
LOCATIONS, LOCATIONS,
@ -31,24 +33,41 @@ export default function QuestionsHomePage() {
selectedQuestionTypes, selectedQuestionTypes,
setSelectedQuestionTypes, setSelectedQuestionTypes,
areQuestionTypesInitialized, areQuestionTypesInitialized,
] = useSearchFilter<QuestionsQuestionType>('questionTypes'); ] = useSearchFilter<QuestionsQuestionType>('questionTypes', {
queryParamToValue: (param) => {
return param.toUpperCase() as QuestionsQuestionType;
},
});
const [ const [
selectedQuestionAge, selectedQuestionAge,
setSelectedQuestionAge, setSelectedQuestionAge,
isQuestionAgeInitialized, isQuestionAgeInitialized,
] = useSearchFilterSingle<string>('questionAge', 'all'); ] = useSearchFilterSingle<QuestionAge>('questionAge', {
defaultValue: 'all',
});
const [selectedLocations, setSelectedLocations, areLocationsInitialized] = const [selectedLocations, setSelectedLocations, areLocationsInitialized] =
useSearchFilter('locations'); 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([ const { data: questions } = trpc.useQuery([
'questions.questions.getQuestionsByFilter', 'questions.questions.getQuestionsByFilter',
{ {
// TODO: Update when query accepts multiple question types companies: selectedCompanies,
questionType: endDate: today,
selectedQuestionTypes.length > 0 locations: selectedLocations,
? (selectedQuestionTypes[0].toUpperCase() as QuestionsQuestionType) questionTypes: selectedQuestionTypes,
: 'CODING', roles: [],
startDate,
}, },
]); ]);
@ -214,6 +233,7 @@ export default function QuestionsHomePage() {
content: data.questionContent, content: data.questionContent,
location: data.location, location: data.location,
questionType: data.questionType, questionType: data.questionType,
role: data.role,
seenAt: data.date, seenAt: data.date,
}); });
}} }}

View File

@ -9,10 +9,12 @@ import type { Question } from '~/types/questions';
export const questionsQuestionRouter = createProtectedRouter() export const questionsQuestionRouter = createProtectedRouter()
.query('getQuestionsByFilter', { .query('getQuestionsByFilter', {
input: z.object({ input: z.object({
company: z.string().optional(), companies: z.string().array(),
location: z.string().optional(), endDate: z.date(),
questionType: z.nativeEnum(QuestionsQuestionType), locations: z.string().array(),
role: z.string().optional(), questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
roles: z.string().array(),
startDate: z.date().optional(),
}), }),
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const questionsData = await ctx.prisma.questionsQuestion.findMany({ const questionsData = await ctx.prisma.questionsQuestion.findMany({
@ -42,7 +44,13 @@ export const questionsQuestionRouter = createProtectedRouter()
createdAt: 'desc', createdAt: 'desc',
}, },
where: { where: {
questionType: input.questionType, ...(input.questionTypes.length > 0
? {
questionType: {
in: input.questionTypes,
},
}
: {}),
}, },
}); });
return questionsData return questionsData
@ -50,11 +58,17 @@ export const questionsQuestionRouter = createProtectedRouter()
for (let i = 0; i < data.encounters.length; i++) { for (let i = 0; i < data.encounters.length; i++) {
const encounter = data.encounters[i]; const encounter = data.encounters[i];
const matchCompany = const matchCompany =
!input.company || encounter.company === input.company; input.companies.length === 0 ||
input.companies.includes(encounter.company);
const matchLocation = const matchLocation =
!input.location || encounter.location === input.location; input.locations.length === 0 ||
const matchRole = !input.company || encounter.role === input.role; input.locations.includes(encounter.location);
if (matchCompany && matchLocation && matchRole) { 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; return true;
} }
} }
@ -174,7 +188,7 @@ export const questionsQuestionRouter = createProtectedRouter()
content: z.string(), content: z.string(),
location: z.string(), location: z.string(),
questionType: z.nativeEnum(QuestionsQuestionType), questionType: z.nativeEnum(QuestionsQuestionType),
role: z.string().optional(), role: z.string(),
seenAt: z.date(), seenAt: z.date(),
}), }),
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
@ -183,6 +197,17 @@ export const questionsQuestionRouter = createProtectedRouter()
const question = await ctx.prisma.questionsQuestion.create({ const question = await ctx.prisma.questionsQuestion.create({
data: { data: {
content: input.content, content: input.content,
encounters: {
create: [
{
company: input.company,
location: input.location,
role: input.role,
seenAt: input.seenAt,
userId,
},
],
},
questionType: input.questionType, questionType: input.questionType,
userId, userId,
}, },

View File

@ -5,13 +5,13 @@ import type { FilterChoices } from '~/components/questions/filter/FilterSection'
export const COMPANIES: FilterChoices = [ export const COMPANIES: FilterChoices = [
{ {
label: 'Google', label: 'Google',
value: 'google', value: 'Google',
}, },
{ {
label: 'Meta', label: 'Meta',
value: 'meta', value: 'Meta',
}, },
]; ] as const;
// Code, design, behavioral // Code, design, behavioral
export const QUESTION_TYPES: FilterChoices<QuestionsQuestionType> = [ export const QUESTION_TYPES: FilterChoices<QuestionsQuestionType> = [
@ -27,9 +27,11 @@ export const QUESTION_TYPES: FilterChoices<QuestionsQuestionType> = [
label: 'Behavioral', label: 'Behavioral',
value: '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<QuestionAge> = [
{ {
label: 'Last month', label: 'Last month',
value: 'last-month', value: 'last-month',
@ -46,16 +48,16 @@ export const QUESTION_AGES: FilterChoices = [
label: 'All', label: 'All',
value: 'all', value: 'all',
}, },
]; ] as const;
export const LOCATIONS: FilterChoices = [ export const LOCATIONS: FilterChoices = [
{ {
label: 'Singapore', label: 'Singapore',
value: 'singapore', value: 'Singapore',
}, },
{ {
label: 'Menlo Park', label: 'Menlo Park',
value: 'menlopark', value: 'Menlo Park',
}, },
{ {
label: 'California', label: 'California',
@ -63,13 +65,13 @@ export const LOCATIONS: FilterChoices = [
}, },
{ {
label: 'Hong Kong', label: 'Hong Kong',
value: 'hongkong', value: 'Hong Kong',
}, },
{ {
label: 'Taiwan', label: 'Taiwan',
value: 'taiwan', value: 'Taiwan',
}, },
]; ] as const;
export const SAMPLE_QUESTION = { export const SAMPLE_QUESTION = {
answerCount: 10, answerCount: 10,

View File

@ -3,8 +3,12 @@ import { useCallback, useEffect, useState } from 'react';
export const useSearchFilter = <Value extends string = string>( export const useSearchFilter = <Value extends string = string>(
name: string, name: string,
defaultValues?: Array<Value>, opts: {
defaultValues?: Array<Value>;
queryParamToValue?: (param: string) => Value;
} = {},
) => { ) => {
const { defaultValues, queryParamToValue = (param) => param } = opts;
const [isInitialized, setIsInitialized] = useState(false); const [isInitialized, setIsInitialized] = useState(false);
const router = useRouter(); const router = useRouter();
@ -16,7 +20,7 @@ export const useSearchFilter = <Value extends string = string>(
const query = router.query[name]; const query = router.query[name];
if (query) { if (query) {
const queryValues = Array.isArray(query) ? query : [query]; const queryValues = Array.isArray(query) ? query : [query];
setFilters(queryValues as Array<Value>); setFilters(queryValues.map(queryParamToValue) as Array<Value>);
} else { } else {
// Try to load from local storage // Try to load from local storage
const localStorageValue = localStorage.getItem(name); const localStorageValue = localStorage.getItem(name);
@ -34,7 +38,7 @@ export const useSearchFilter = <Value extends string = string>(
} }
setIsInitialized(true); setIsInitialized(true);
} }
}, [isInitialized, name, router]); }, [isInitialized, name, queryParamToValue, router]);
const setFiltersCallback = useCallback( const setFiltersCallback = useCallback(
(newFilters: Array<Value>) => { (newFilters: Array<Value>) => {
@ -56,11 +60,16 @@ export const useSearchFilter = <Value extends string = string>(
export const useSearchFilterSingle = <Value extends string = string>( export const useSearchFilterSingle = <Value extends string = string>(
name: string, name: string,
defaultValue: Value, opts: {
defaultValue?: Value;
queryParamToValue?: (param: string) => Value;
} = {},
) => { ) => {
const [filters, setFilters, isInitialized] = useSearchFilter(name, [ const { defaultValue, queryParamToValue } = opts;
defaultValue, const [filters, setFilters, isInitialized] = useSearchFilter(name, {
]); defaultValues: defaultValue !== undefined ? [defaultValue] : undefined,
queryParamToValue,
});
return [ return [
filters[0], filters[0],