[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?
// 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

View File

@ -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({
<div className="min-w-[150px] max-w-[300px] flex-1">
<TextInput
label="Location"
required={true}
startAddOn={CalendarDaysIcon}
startAddOnType="icon"
{...register('location')}
/>
</div>
{/* <div className="min-w-[150px] max-w-[200px] flex-1">
<div className="min-w-[150px] max-w-[200px] flex-1">
<TextInput
label="Position <TODO>"
label="Role"
required={true}
startAddOn={UserIcon}
startAddOnType="icon"
{...register('position')}
{...register('role')}
/>
</div> */}
</div>
</div>
</Collapsible>
{/* <div className="w-full">

View File

@ -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<LandingQueryData>({
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);
}}
/>
</div>
<p>questions from</p>

View File

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

View File

@ -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<QuestionsQuestionType>('questionTypes');
] = useSearchFilter<QuestionsQuestionType>('questionTypes', {
queryParamToValue: (param) => {
return param.toUpperCase() as QuestionsQuestionType;
},
});
const [
selectedQuestionAge,
setSelectedQuestionAge,
isQuestionAgeInitialized,
] = useSearchFilterSingle<string>('questionAge', 'all');
] = useSearchFilterSingle<QuestionAge>('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,
});
}}

View File

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

View File

@ -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<QuestionsQuestionType> = [
@ -27,9 +27,11 @@ export const QUESTION_TYPES: FilterChoices<QuestionsQuestionType> = [
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<QuestionAge> = [
{
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,

View File

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