mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-28 20:52:00 +08:00
[questions][feat] add nested create encounter (#339)
Co-authored-by: Jeff Sieu <jeffsy00@gmail.com>
This commit is contained in:
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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'>
|
||||
>;
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
}}
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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],
|
||||
|
Reference in New Issue
Block a user