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?
|
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
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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'>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
|
@ -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],
|
||||||
|
Reference in New Issue
Block a user