parent
1361c5bfab
commit
db8379bdc8
@ -0,0 +1,405 @@
|
|||||||
|
import { subMonths, subYears } from 'date-fns';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import Router, { useRouter } from 'next/router';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { NoSymbolIcon } from '@heroicons/react/24/outline';
|
||||||
|
import type { QuestionsQuestionType } from '@prisma/client';
|
||||||
|
import { SlideOut, Typeahead } from '@tih/ui';
|
||||||
|
|
||||||
|
import QuestionOverviewCard from '~/components/questions/card/question/QuestionOverviewCard';
|
||||||
|
import ContributeQuestionCard from '~/components/questions/ContributeQuestionCard';
|
||||||
|
import FilterSection from '~/components/questions/filter/FilterSection';
|
||||||
|
import QuestionSearchBar from '~/components/questions/QuestionSearchBar';
|
||||||
|
|
||||||
|
import type { QuestionAge } from '~/utils/questions/constants';
|
||||||
|
import { APP_TITLE } from '~/utils/questions/constants';
|
||||||
|
import { ROLES } from '~/utils/questions/constants';
|
||||||
|
import {
|
||||||
|
COMPANIES,
|
||||||
|
LOCATIONS,
|
||||||
|
QUESTION_AGES,
|
||||||
|
QUESTION_TYPES,
|
||||||
|
} from '~/utils/questions/constants';
|
||||||
|
import createSlug from '~/utils/questions/createSlug';
|
||||||
|
import {
|
||||||
|
useSearchFilter,
|
||||||
|
useSearchFilterSingle,
|
||||||
|
} from '~/utils/questions/useSearchFilter';
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
export default function QuestionsBrowsePage() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [selectedCompanies, setSelectedCompanies, areCompaniesInitialized] =
|
||||||
|
useSearchFilter('companies');
|
||||||
|
const [
|
||||||
|
selectedQuestionTypes,
|
||||||
|
setSelectedQuestionTypes,
|
||||||
|
areQuestionTypesInitialized,
|
||||||
|
] = useSearchFilter<QuestionsQuestionType>('questionTypes', {
|
||||||
|
queryParamToValue: (param) => {
|
||||||
|
const uppercaseParam = param.toUpperCase();
|
||||||
|
return (
|
||||||
|
QUESTION_TYPES.find(
|
||||||
|
(questionType) => questionType.value.toUpperCase() === uppercaseParam,
|
||||||
|
)?.value ?? null
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [
|
||||||
|
selectedQuestionAge,
|
||||||
|
setSelectedQuestionAge,
|
||||||
|
isQuestionAgeInitialized,
|
||||||
|
] = useSearchFilterSingle<QuestionAge>('questionAge', {
|
||||||
|
defaultValue: 'all',
|
||||||
|
queryParamToValue: (param) => {
|
||||||
|
const uppercaseParam = param.toUpperCase();
|
||||||
|
return (
|
||||||
|
QUESTION_AGES.find(
|
||||||
|
(questionAge) => questionAge.value.toUpperCase() === uppercaseParam,
|
||||||
|
)?.value ?? null
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedRoles, setSelectedRoles, areRolesInitialized] =
|
||||||
|
useSearchFilter('roles');
|
||||||
|
const [selectedLocations, setSelectedLocations, areLocationsInitialized] =
|
||||||
|
useSearchFilter('locations');
|
||||||
|
|
||||||
|
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',
|
||||||
|
{
|
||||||
|
companyNames: selectedCompanies,
|
||||||
|
endDate: today,
|
||||||
|
locations: selectedLocations,
|
||||||
|
questionTypes: selectedQuestionTypes,
|
||||||
|
roles: selectedRoles,
|
||||||
|
startDate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const { mutate: createQuestion } = trpc.useMutation(
|
||||||
|
'questions.questions.create',
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.invalidateQueries('questions.questions.getQuestionsByFilter');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
const [filterDrawerOpen, setFilterDrawerOpen] = useState(false);
|
||||||
|
|
||||||
|
const companyFilterOptions = useMemo(() => {
|
||||||
|
return COMPANIES.map((company) => ({
|
||||||
|
...company,
|
||||||
|
checked: selectedCompanies.includes(company.value),
|
||||||
|
}));
|
||||||
|
}, [selectedCompanies]);
|
||||||
|
|
||||||
|
const questionTypeFilterOptions = useMemo(() => {
|
||||||
|
return QUESTION_TYPES.map((questionType) => ({
|
||||||
|
...questionType,
|
||||||
|
checked: selectedQuestionTypes.includes(questionType.value),
|
||||||
|
}));
|
||||||
|
}, [selectedQuestionTypes]);
|
||||||
|
|
||||||
|
const questionAgeFilterOptions = useMemo(() => {
|
||||||
|
return QUESTION_AGES.map((questionAge) => ({
|
||||||
|
...questionAge,
|
||||||
|
checked: selectedQuestionAge === questionAge.value,
|
||||||
|
}));
|
||||||
|
}, [selectedQuestionAge]);
|
||||||
|
|
||||||
|
const roleFilterOptions = useMemo(() => {
|
||||||
|
return ROLES.map((role) => ({
|
||||||
|
...role,
|
||||||
|
checked: selectedRoles.includes(role.value),
|
||||||
|
}));
|
||||||
|
}, [selectedRoles]);
|
||||||
|
|
||||||
|
const locationFilterOptions = useMemo(() => {
|
||||||
|
return LOCATIONS.map((location) => ({
|
||||||
|
...location,
|
||||||
|
checked: selectedLocations.includes(location.value),
|
||||||
|
}));
|
||||||
|
}, [selectedLocations]);
|
||||||
|
|
||||||
|
const areFiltersInitialized = useMemo(() => {
|
||||||
|
return (
|
||||||
|
areCompaniesInitialized &&
|
||||||
|
areQuestionTypesInitialized &&
|
||||||
|
isQuestionAgeInitialized &&
|
||||||
|
areRolesInitialized &&
|
||||||
|
areLocationsInitialized
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
areCompaniesInitialized,
|
||||||
|
areQuestionTypesInitialized,
|
||||||
|
isQuestionAgeInitialized,
|
||||||
|
areRolesInitialized,
|
||||||
|
areLocationsInitialized,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { pathname } = router;
|
||||||
|
useEffect(() => {
|
||||||
|
if (areFiltersInitialized) {
|
||||||
|
// Router.replace used instead of router.replace to avoid
|
||||||
|
// the page reloading itself since the router.replace
|
||||||
|
// callback changes on every page load
|
||||||
|
Router.replace({
|
||||||
|
pathname,
|
||||||
|
query: {
|
||||||
|
companies: selectedCompanies,
|
||||||
|
locations: selectedLocations,
|
||||||
|
questionAge: selectedQuestionAge,
|
||||||
|
questionTypes: selectedQuestionTypes,
|
||||||
|
roles: selectedRoles,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoaded(true);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
areFiltersInitialized,
|
||||||
|
loaded,
|
||||||
|
pathname,
|
||||||
|
selectedCompanies,
|
||||||
|
selectedRoles,
|
||||||
|
selectedLocations,
|
||||||
|
selectedQuestionAge,
|
||||||
|
selectedQuestionTypes,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const filterSidebar = (
|
||||||
|
<div className="mt-2 divide-y divide-slate-200 px-4">
|
||||||
|
<FilterSection
|
||||||
|
label="Company"
|
||||||
|
options={companyFilterOptions}
|
||||||
|
renderInput={({
|
||||||
|
onOptionChange,
|
||||||
|
options,
|
||||||
|
field: { ref: _, ...field },
|
||||||
|
}) => (
|
||||||
|
<Typeahead
|
||||||
|
{...field}
|
||||||
|
label=""
|
||||||
|
options={options}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
onQueryChange={() => {}}
|
||||||
|
onSelect={({ value }) => {
|
||||||
|
onOptionChange(value, true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
onOptionChange={(optionValue, checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedCompanies([...selectedCompanies, optionValue]);
|
||||||
|
} else {
|
||||||
|
setSelectedCompanies(
|
||||||
|
selectedCompanies.filter((company) => company !== optionValue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FilterSection
|
||||||
|
label="Question types"
|
||||||
|
options={questionTypeFilterOptions}
|
||||||
|
showAll={true}
|
||||||
|
onOptionChange={(optionValue, checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedQuestionTypes([...selectedQuestionTypes, optionValue]);
|
||||||
|
} else {
|
||||||
|
setSelectedQuestionTypes(
|
||||||
|
selectedQuestionTypes.filter(
|
||||||
|
(questionType) => questionType !== optionValue,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FilterSection
|
||||||
|
isSingleSelect={true}
|
||||||
|
label="Question age"
|
||||||
|
options={questionAgeFilterOptions}
|
||||||
|
showAll={true}
|
||||||
|
onOptionChange={(optionValue) => {
|
||||||
|
setSelectedQuestionAge(optionValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FilterSection
|
||||||
|
label="Roles"
|
||||||
|
options={roleFilterOptions}
|
||||||
|
renderInput={({
|
||||||
|
onOptionChange,
|
||||||
|
options,
|
||||||
|
field: { ref: _, ...field },
|
||||||
|
}) => (
|
||||||
|
<Typeahead
|
||||||
|
{...field}
|
||||||
|
label=""
|
||||||
|
options={options}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
onQueryChange={() => {}}
|
||||||
|
onSelect={({ value }) => {
|
||||||
|
onOptionChange(value, true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
onOptionChange={(optionValue, checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedRoles([...selectedRoles, optionValue]);
|
||||||
|
} else {
|
||||||
|
setSelectedRoles(
|
||||||
|
selectedRoles.filter((role) => role !== optionValue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FilterSection
|
||||||
|
label="Location"
|
||||||
|
options={locationFilterOptions}
|
||||||
|
renderInput={({
|
||||||
|
onOptionChange,
|
||||||
|
options,
|
||||||
|
field: { ref: _, ...field },
|
||||||
|
}) => (
|
||||||
|
<Typeahead
|
||||||
|
{...field}
|
||||||
|
label=""
|
||||||
|
options={options}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
onQueryChange={() => {}}
|
||||||
|
onSelect={({ value }) => {
|
||||||
|
onOptionChange(value, true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
onOptionChange={(optionValue, checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedLocations([...selectedLocations, optionValue]);
|
||||||
|
} else {
|
||||||
|
setSelectedLocations(
|
||||||
|
selectedLocations.filter((location) => location !== optionValue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Home - {APP_TITLE}</title>
|
||||||
|
</Head>
|
||||||
|
<main className="flex flex-1 flex-col items-stretch">
|
||||||
|
<div className="flex h-full flex-1">
|
||||||
|
<section className="flex min-h-0 flex-1 flex-col items-center overflow-auto">
|
||||||
|
<div className="flex min-h-0 max-w-3xl flex-1 p-4">
|
||||||
|
<div className="flex flex-1 flex-col items-stretch justify-start gap-4">
|
||||||
|
<ContributeQuestionCard
|
||||||
|
onSubmit={(data) => {
|
||||||
|
createQuestion({
|
||||||
|
companyId: data.company,
|
||||||
|
content: data.questionContent,
|
||||||
|
location: data.location,
|
||||||
|
questionType: data.questionType,
|
||||||
|
role: data.role,
|
||||||
|
seenAt: data.date,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<QuestionSearchBar
|
||||||
|
sortOptions={[
|
||||||
|
{
|
||||||
|
label: 'Most recent',
|
||||||
|
value: 'most-recent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Most upvotes',
|
||||||
|
value: 'most-upvotes',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
sortValue="most-recent"
|
||||||
|
onFilterOptionsToggle={() => {
|
||||||
|
setFilterDrawerOpen(!filterDrawerOpen);
|
||||||
|
}}
|
||||||
|
onSortChange={(value) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-4 pb-4">
|
||||||
|
{(questions ?? []).map((question) => (
|
||||||
|
<QuestionOverviewCard
|
||||||
|
key={question.id}
|
||||||
|
answerCount={question.numAnswers}
|
||||||
|
companies={{ [question.company]: 1 }}
|
||||||
|
content={question.content}
|
||||||
|
href={`/questions/${question.id}/${createSlug(
|
||||||
|
question.content,
|
||||||
|
)}`}
|
||||||
|
locations={{ [question.location]: 1 }}
|
||||||
|
questionId={question.id}
|
||||||
|
receivedCount={question.receivedCount}
|
||||||
|
roles={{ [question.role]: 1 }}
|
||||||
|
timestamp={question.seenAt.toLocaleDateString(undefined, {
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
})}
|
||||||
|
type={question.type}
|
||||||
|
upvoteCount={question.numVotes}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{questions?.length === 0 && (
|
||||||
|
<div className="flex w-full items-center justify-center gap-2 rounded-md border border-slate-300 bg-slate-200 p-4 text-slate-600">
|
||||||
|
<NoSymbolIcon className="h-6 w-6" />
|
||||||
|
<p>Nothing found. Try changing your search filters.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<aside className="hidden w-[300px] overflow-y-auto border-l bg-white py-4 lg:block">
|
||||||
|
<h2 className="px-4 text-xl font-semibold">Filter by</h2>
|
||||||
|
{filterSidebar}
|
||||||
|
</aside>
|
||||||
|
<SlideOut
|
||||||
|
className="lg:hidden"
|
||||||
|
enterFrom="end"
|
||||||
|
isShown={filterDrawerOpen}
|
||||||
|
size="sm"
|
||||||
|
title="Filter by"
|
||||||
|
onClose={() => {
|
||||||
|
setFilterDrawerOpen(false);
|
||||||
|
}}>
|
||||||
|
{filterSidebar}
|
||||||
|
</SlideOut>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,435 +1,34 @@
|
|||||||
import { subMonths, subYears } from 'date-fns';
|
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Router, { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
import { NoSymbolIcon } from '@heroicons/react/24/outline';
|
|
||||||
import type { QuestionsQuestionType } from '@prisma/client';
|
|
||||||
import { SlideOut, Typeahead } from '@tih/ui';
|
|
||||||
|
|
||||||
import QuestionOverviewCard from '~/components/questions/card/question/QuestionOverviewCard';
|
|
||||||
import ContributeQuestionCard from '~/components/questions/ContributeQuestionCard';
|
|
||||||
import FilterSection from '~/components/questions/filter/FilterSection';
|
|
||||||
import type { LandingQueryData } from '~/components/questions/LandingComponent';
|
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 type { QuestionAge } from '~/utils/questions/constants';
|
|
||||||
import { APP_TITLE } from '~/utils/questions/constants';
|
import { APP_TITLE } from '~/utils/questions/constants';
|
||||||
import { ROLES } from '~/utils/questions/constants';
|
|
||||||
import {
|
|
||||||
COMPANIES,
|
|
||||||
LOCATIONS,
|
|
||||||
QUESTION_AGES,
|
|
||||||
QUESTION_TYPES,
|
|
||||||
} from '~/utils/questions/constants';
|
|
||||||
import createSlug from '~/utils/questions/createSlug';
|
|
||||||
import {
|
|
||||||
useSearchFilter,
|
|
||||||
useSearchFilterSingle,
|
|
||||||
} from '~/utils/questions/useSearchFilter';
|
|
||||||
import { trpc } from '~/utils/trpc';
|
|
||||||
|
|
||||||
export default function QuestionsHomePage() {
|
export default function QuestionsHomePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [selectedCompanies, setSelectedCompanies, areCompaniesInitialized] =
|
|
||||||
useSearchFilter('companies');
|
|
||||||
const [
|
|
||||||
selectedQuestionTypes,
|
|
||||||
setSelectedQuestionTypes,
|
|
||||||
areQuestionTypesInitialized,
|
|
||||||
] = useSearchFilter<QuestionsQuestionType>('questionTypes', {
|
|
||||||
queryParamToValue: (param) => {
|
|
||||||
const uppercaseParam = param.toUpperCase();
|
|
||||||
return (
|
|
||||||
QUESTION_TYPES.find(
|
|
||||||
(questionType) => questionType.value.toUpperCase() === uppercaseParam,
|
|
||||||
)?.value ?? null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [
|
|
||||||
selectedQuestionAge,
|
|
||||||
setSelectedQuestionAge,
|
|
||||||
isQuestionAgeInitialized,
|
|
||||||
] = useSearchFilterSingle<QuestionAge>('questionAge', {
|
|
||||||
defaultValue: 'all',
|
|
||||||
queryParamToValue: (param) => {
|
|
||||||
const uppercaseParam = param.toUpperCase();
|
|
||||||
return (
|
|
||||||
QUESTION_AGES.find(
|
|
||||||
(questionAge) => questionAge.value.toUpperCase() === uppercaseParam,
|
|
||||||
)?.value ?? null
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [selectedRoles, setSelectedRoles, areRolesInitialized] =
|
|
||||||
useSearchFilter('roles');
|
|
||||||
const [selectedLocations, setSelectedLocations, areLocationsInitialized] =
|
|
||||||
useSearchFilter('locations');
|
|
||||||
|
|
||||||
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',
|
|
||||||
{
|
|
||||||
companyNames: selectedCompanies,
|
|
||||||
endDate: today,
|
|
||||||
locations: selectedLocations,
|
|
||||||
questionTypes: selectedQuestionTypes,
|
|
||||||
roles: selectedRoles,
|
|
||||||
startDate,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
keepPreviousData: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
const { mutate: createQuestion } = trpc.useMutation(
|
|
||||||
'questions.questions.create',
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
utils.invalidateQueries('questions.questions.getQuestionsByFilter');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const [hasLanded, setHasLanded] = useState(false);
|
|
||||||
const [loaded, setLoaded] = useState(false);
|
|
||||||
const [filterDrawerOpen, setFilterDrawerOpen] = useState(false);
|
|
||||||
|
|
||||||
const companyFilterOptions = useMemo(() => {
|
|
||||||
return COMPANIES.map((company) => ({
|
|
||||||
...company,
|
|
||||||
checked: selectedCompanies.includes(company.value),
|
|
||||||
}));
|
|
||||||
}, [selectedCompanies]);
|
|
||||||
|
|
||||||
const questionTypeFilterOptions = useMemo(() => {
|
|
||||||
return QUESTION_TYPES.map((questionType) => ({
|
|
||||||
...questionType,
|
|
||||||
checked: selectedQuestionTypes.includes(questionType.value),
|
|
||||||
}));
|
|
||||||
}, [selectedQuestionTypes]);
|
|
||||||
|
|
||||||
const questionAgeFilterOptions = useMemo(() => {
|
|
||||||
return QUESTION_AGES.map((questionAge) => ({
|
|
||||||
...questionAge,
|
|
||||||
checked: selectedQuestionAge === questionAge.value,
|
|
||||||
}));
|
|
||||||
}, [selectedQuestionAge]);
|
|
||||||
|
|
||||||
const roleFilterOptions = useMemo(() => {
|
|
||||||
return ROLES.map((role) => ({
|
|
||||||
...role,
|
|
||||||
checked: selectedRoles.includes(role.value),
|
|
||||||
}));
|
|
||||||
}, [selectedRoles]);
|
|
||||||
|
|
||||||
const locationFilterOptions = useMemo(() => {
|
|
||||||
return LOCATIONS.map((location) => ({
|
|
||||||
...location,
|
|
||||||
checked: selectedLocations.includes(location.value),
|
|
||||||
}));
|
|
||||||
}, [selectedLocations]);
|
|
||||||
|
|
||||||
const handleLandingQuery = async (data: LandingQueryData) => {
|
const handleLandingQuery = async (data: LandingQueryData) => {
|
||||||
const { company, location, questionType } = data;
|
const { company, location, questionType } = data;
|
||||||
|
|
||||||
setSelectedCompanies([company]);
|
// Go to browse page
|
||||||
setSelectedRoles([]);
|
router.push({
|
||||||
setSelectedLocations([location]);
|
pathname: '/questions/browse',
|
||||||
setSelectedQuestionTypes([questionType as QuestionsQuestionType]);
|
query: {
|
||||||
setHasLanded(true);
|
companies: [company],
|
||||||
|
locations: [location],
|
||||||
|
questionTypes: [questionType],
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const areFiltersInitialized = useMemo(() => {
|
|
||||||
return (
|
|
||||||
areCompaniesInitialized &&
|
|
||||||
areQuestionTypesInitialized &&
|
|
||||||
isQuestionAgeInitialized &&
|
|
||||||
areRolesInitialized &&
|
|
||||||
areLocationsInitialized
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
areCompaniesInitialized,
|
|
||||||
areQuestionTypesInitialized,
|
|
||||||
isQuestionAgeInitialized,
|
|
||||||
areRolesInitialized,
|
|
||||||
areLocationsInitialized,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { pathname } = router;
|
|
||||||
useEffect(() => {
|
|
||||||
if (areFiltersInitialized) {
|
|
||||||
// Router.replace used instead of router.replace to avoid
|
|
||||||
// the page reloading itself since the router.replace
|
|
||||||
// callback changes on every page load
|
|
||||||
Router.replace({
|
|
||||||
pathname,
|
|
||||||
query: {
|
|
||||||
companies: selectedCompanies,
|
|
||||||
locations: selectedLocations,
|
|
||||||
questionAge: selectedQuestionAge,
|
|
||||||
questionTypes: selectedQuestionTypes,
|
|
||||||
roles: selectedRoles,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const hasFilter =
|
|
||||||
selectedCompanies.length > 0 ||
|
|
||||||
selectedRoles.length > 0 ||
|
|
||||||
selectedLocations.length > 0 ||
|
|
||||||
selectedQuestionAge !== 'all' ||
|
|
||||||
selectedQuestionTypes.length > 0;
|
|
||||||
if (hasFilter) {
|
|
||||||
setHasLanded(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoaded(true);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
areFiltersInitialized,
|
|
||||||
hasLanded,
|
|
||||||
loaded,
|
|
||||||
pathname,
|
|
||||||
selectedCompanies,
|
|
||||||
selectedRoles,
|
|
||||||
selectedLocations,
|
|
||||||
selectedQuestionAge,
|
|
||||||
selectedQuestionTypes,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!loaded) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const filterSidebar = (
|
|
||||||
<div className="mt-2 divide-y divide-slate-200 px-4">
|
|
||||||
<FilterSection
|
|
||||||
label="Company"
|
|
||||||
options={companyFilterOptions}
|
|
||||||
renderInput={({
|
|
||||||
onOptionChange,
|
|
||||||
options,
|
|
||||||
field: { ref: _, ...field },
|
|
||||||
}) => (
|
|
||||||
<Typeahead
|
|
||||||
{...field}
|
|
||||||
label=""
|
|
||||||
options={options}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
onQueryChange={() => {}}
|
|
||||||
onSelect={({ value }) => {
|
|
||||||
onOptionChange(value, true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
onOptionChange={(optionValue, checked) => {
|
|
||||||
if (checked) {
|
|
||||||
setSelectedCompanies([...selectedCompanies, optionValue]);
|
|
||||||
} else {
|
|
||||||
setSelectedCompanies(
|
|
||||||
selectedCompanies.filter((company) => company !== optionValue),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FilterSection
|
|
||||||
label="Question types"
|
|
||||||
options={questionTypeFilterOptions}
|
|
||||||
showAll={true}
|
|
||||||
onOptionChange={(optionValue, checked) => {
|
|
||||||
if (checked) {
|
|
||||||
setSelectedQuestionTypes([...selectedQuestionTypes, optionValue]);
|
|
||||||
} else {
|
|
||||||
setSelectedQuestionTypes(
|
|
||||||
selectedQuestionTypes.filter(
|
|
||||||
(questionType) => questionType !== optionValue,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FilterSection
|
|
||||||
isSingleSelect={true}
|
|
||||||
label="Question age"
|
|
||||||
options={questionAgeFilterOptions}
|
|
||||||
showAll={true}
|
|
||||||
onOptionChange={(optionValue) => {
|
|
||||||
setSelectedQuestionAge(optionValue);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FilterSection
|
|
||||||
label="Roles"
|
|
||||||
options={roleFilterOptions}
|
|
||||||
renderInput={({
|
|
||||||
onOptionChange,
|
|
||||||
options,
|
|
||||||
field: { ref: _, ...field },
|
|
||||||
}) => (
|
|
||||||
<Typeahead
|
|
||||||
{...field}
|
|
||||||
label=""
|
|
||||||
options={options}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
onQueryChange={() => {}}
|
|
||||||
onSelect={({ value }) => {
|
|
||||||
onOptionChange(value, true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
onOptionChange={(optionValue, checked) => {
|
|
||||||
if (checked) {
|
|
||||||
setSelectedRoles([...selectedRoles, optionValue]);
|
|
||||||
} else {
|
|
||||||
setSelectedRoles(
|
|
||||||
selectedRoles.filter((role) => role !== optionValue),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FilterSection
|
|
||||||
label="Location"
|
|
||||||
options={locationFilterOptions}
|
|
||||||
renderInput={({
|
|
||||||
onOptionChange,
|
|
||||||
options,
|
|
||||||
field: { ref: _, ...field },
|
|
||||||
}) => (
|
|
||||||
<Typeahead
|
|
||||||
{...field}
|
|
||||||
label=""
|
|
||||||
options={options}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
||||||
onQueryChange={() => {}}
|
|
||||||
onSelect={({ value }) => {
|
|
||||||
onOptionChange(value, true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
onOptionChange={(optionValue, checked) => {
|
|
||||||
if (checked) {
|
|
||||||
setSelectedLocations([...selectedLocations, optionValue]);
|
|
||||||
} else {
|
|
||||||
setSelectedLocations(
|
|
||||||
selectedLocations.filter((location) => location !== optionValue),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Home - {APP_TITLE}</title>
|
<title>Home - {APP_TITLE}</title>
|
||||||
</Head>
|
</Head>
|
||||||
{!hasLanded ? (
|
<LandingComponent onLanded={handleLandingQuery}></LandingComponent>
|
||||||
<LandingComponent onLanded={handleLandingQuery}></LandingComponent>
|
|
||||||
) : (
|
|
||||||
<main className="flex flex-1 flex-col items-stretch">
|
|
||||||
<div className="flex h-full flex-1">
|
|
||||||
<section className="flex min-h-0 flex-1 flex-col items-center overflow-auto">
|
|
||||||
<div className="flex min-h-0 max-w-3xl flex-1 p-4">
|
|
||||||
<div className="flex flex-1 flex-col items-stretch justify-start gap-4">
|
|
||||||
<ContributeQuestionCard
|
|
||||||
onSubmit={(data) => {
|
|
||||||
createQuestion({
|
|
||||||
companyId: data.company,
|
|
||||||
content: data.questionContent,
|
|
||||||
location: data.location,
|
|
||||||
questionType: data.questionType,
|
|
||||||
role: data.role,
|
|
||||||
seenAt: data.date,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<QuestionSearchBar
|
|
||||||
sortOptions={[
|
|
||||||
{
|
|
||||||
label: 'Most recent',
|
|
||||||
value: 'most-recent',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Most upvotes',
|
|
||||||
value: 'most-upvotes',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
sortValue="most-recent"
|
|
||||||
onFilterOptionsToggle={() => {
|
|
||||||
setFilterDrawerOpen(!filterDrawerOpen);
|
|
||||||
}}
|
|
||||||
onSortChange={(value) => {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-4 pb-4">
|
|
||||||
{(questions ?? []).map((question) => (
|
|
||||||
<QuestionOverviewCard
|
|
||||||
key={question.id}
|
|
||||||
answerCount={question.numAnswers}
|
|
||||||
companies={{ [question.company]: 1 }}
|
|
||||||
content={question.content}
|
|
||||||
href={`/questions/${question.id}/${createSlug(
|
|
||||||
question.content,
|
|
||||||
)}`}
|
|
||||||
locations={{ [question.location]: 1 }}
|
|
||||||
questionId={question.id}
|
|
||||||
receivedCount={question.receivedCount}
|
|
||||||
roles={{ [question.role]: 1 }}
|
|
||||||
timestamp={question.seenAt.toLocaleDateString(
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
month: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
type={question.type}
|
|
||||||
upvoteCount={question.numVotes}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{questions?.length === 0 && (
|
|
||||||
<div className="flex w-full items-center justify-center gap-2 rounded-md border border-slate-300 bg-slate-200 p-4 text-slate-600">
|
|
||||||
<NoSymbolIcon className="h-6 w-6" />
|
|
||||||
<p>Nothing found. Try changing your search filters.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<aside className="hidden w-[300px] overflow-y-auto border-l bg-white py-4 lg:block">
|
|
||||||
<h2 className="px-4 text-xl font-semibold">Filter by</h2>
|
|
||||||
{filterSidebar}
|
|
||||||
</aside>
|
|
||||||
<SlideOut
|
|
||||||
className="lg:hidden"
|
|
||||||
enterFrom="end"
|
|
||||||
isShown={filterDrawerOpen}
|
|
||||||
size="sm"
|
|
||||||
title="Filter by"
|
|
||||||
onClose={() => {
|
|
||||||
setFilterDrawerOpen(false);
|
|
||||||
}}>
|
|
||||||
{filterSidebar}
|
|
||||||
</SlideOut>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in new issue