Skip to content

Instantly share code, notes, and snippets.

@OlegLustenko
Created May 20, 2024 11:48
Show Gist options
  • Save OlegLustenko/c5b1a970dcd0933b2a4bfb966811e413 to your computer and use it in GitHub Desktop.
Save OlegLustenko/c5b1a970dcd0933b2a4bfb966811e413 to your computer and use it in GitHub Desktop.
// infrastructure/api/search
const baseURL = '/api/v1/projects'
const fetchSearchAPI = async (
{
projectSlug,
params,
}: {
projectSlug: string;
params: QueryParams;
},
options: PassedOptions = {},
): Response<SearchResponse> => {
const url = `${baseURL}/${projectSlug}/search`;
try {
return await fetcher(url, {
...options,
params,
});
} catch (error) {
return handleError({ error, origin: 'fetchSearchAPI', url });
}
};
// infrastructure/api/remoteAPI.ts
import * as searchAPI from './search/search.api';
export const remoteAPI = {
search: searchAPI,
};
// infrastructure/feature/Search/@core/search.server-actions.ts
export const getSearchList = async (
projectSlug: string,
searchParams: SearchListParams,
) => {
const {
tab: searchParamTab,
filters: searchParamFilters,
page,
search,
skills,
cities,
industries,
areas,
} = searchParams;
const tab = TAB_MAP[searchParamTab];
const filters = searchParamFilters && FILTERS_MAP[searchParamFilters];
const searchListItems =
await remoteAPI.search.fetchSearchAPI({
projectSlug,
params: {
tab,
search,
filters,
page,
skills,
cities,
industries,
areas,
},
});
assertErrorInResponse(searchListItems, 'getSearchList');
const { items, aggregations, total, pagination } = searchListItems.data;
return {
items: getMappedItems(items),
counters: getApplicantsFiltersCounters(aggregations),
total,
pagination,
};
};
// infrastructure/feature/Search/@core/search.hooks.ts
export const useApplicantsSearchParams = () => {
const [isPending, startTransition] = useTransition();
const searchParams = useSearchParams();
const router = useRouter();
const tabStateRef = useRef<TabStateRef>(tabStateRefInitialState);
const replaceParams = useReplaceParams();
const tab = searchParams!.get('tab') as ApplicantsTabs;
const filters = searchParams!.get('filters') as ApplicantsFilters;
const page = searchParams!.get('page') as string;
const search = searchParams!.get('search') as string;
const cities = searchParams.getAll('cities');
const industries = searchParams.getAll('industries');
const skills = searchParams.getAll('skills');
const areas = searchParams.getAll('areas');
const getNewUrl = ({
newTab = tab,
newFilter = filters,
newPage = page,
newSearch = search,
newCities = cities.length ? cities : [],
newAreas = areas.length ? areas : [],
newSkills = skills.length ? skills : [],
newIndustries = industries.length ? industries : [],
}: ApplicantsUrlParams) => {
return replaceParams({
tab: newTab,
filters: newFilter,
page: newPage,
search: newSearch,
cities: newCities,
areas: newAreas,
industries: newIndustries,
skills: newSkills,
});
};
const updateApplicantsParams = (params: ApplicantsUrlParams) => {
const newUrl = getNewUrl(params);
startTransition(() => {
router.push(newUrl);
});
};
const getFilterUrl = (passedFilter: ApplicantsFilters | null) => {
const newFilter = passedFilter === filters ? null : passedFilter;
return getNewUrl({ newFilter });
};
const setPage = (passedPage: string | null) => {
if (passedPage === page) {
return;
}
let newPage = passedPage;
if (passedPage && page > passedPage && passedPage === '1') {
newPage = null;
}
updateApplicantsParams({ newPage });
};
const setSearch = (passedSearch?: string | null) => {
if (passedSearch === search) {
return;
}
const newSearch = !passedSearch ? null : passedSearch;
updateApplicantsParams({ newSearch, newPage: null });
};
// On tab switch we set tab and clear out all filters.
const getTabUrl = (newTab: ApplicantsTabs) => {
const data = {
newTab,
newFilter: filters,
newSearch: search,
newPage: null,
} as ApplicantsUrlParams;
const tabsState = tabStateRef.current;
if (tab !== newTab) {
data.newFilter = tabsState[newTab].filters;
data.newSearch = tabsState[newTab].search;
}
if (tab === newTab) {
tabsState[newTab] = {
filters: data.newFilter,
search: data.newSearch,
};
}
return getNewUrl(data);
};
const getCityUrl = (passedCities: string[]) => {
// ...
};
const getAreaUrl = (passedArea: string[]) => {
const newAreas = isEqual(passedArea, areas) ? null : passedArea;
return getNewUrl({ newAreas });
};
const setSkills = (passedSkills: string[]) => {
if (isEqual(passedSkills, skills)) {
return;
}
return updateApplicantsParams({ newSkills: passedSkills });
};
const setIndustries = (passedIndustries: string[]) => {
if (isEqual(passedIndustries, industries)) {
return;
}
return updateApplicantsParams({ newIndustries: passedIndustries });
};
const getSkillUrl = (passedSkill: string, selected: boolean) => {
// ..
};
const getIndustryUrl = (passedIndustry: string, selected: boolean) => {
if (industries.includes(passedIndustry)) {
const newIndustries = industries.filter((industry) => {
if (passedIndustry === industry) {
return !selected;
}
return true;
});
return getNewUrl({ newIndustries });
}
return getNewUrl({ newIndustries: [...industries, passedIndustry] });
};
return {
isPending,
tab,
filters,
page,
search,
skills,
areas,
cities,
getFilterUrl,
getNewUrl,
getTabUrl,
getCityUrl,
getAreaUrl,
getIndustryUrl,
setIndustries,
setSkills,
getSkillUrl,
setPage,
setSearch,
};
};
// infrastructure/feature/Search/@core/index.ts
import * as aggregations from './search.aggregations';
import * as constants from './search.constants';
import * as domain from './search.domain';
import * as serverActions from './search.server-actions';
export const searchFeature = {
aggregations,
constants,
serverActions,
domain,
};
// infrastructure/features/Search/SearchList.tsx
type SearchListProps = {
searchParams: SearchListParams;
params: { projectId: string };
};
export const SearchList = async ({
searchParams,
params,
}: SearchListProps) => {
const searchResults =
await searchFeature.serverActions.getSearchList(
params.projectId,
searchParams,
);
const activeTabCounter = searchResults.counters;
const hasTabApplicants = Boolean(activeTabCounter.total);
const noApplicantsUnderThisTab =
!activeTabCounter.total && !searchParams.search;
const noApplicantsAfterSearch =
!activeTabCounter.total && searchParams.search;
const shouldSearchBeDisplayed = hasTabApplicants || noApplicantsAfterSearch;
return (
<Page.Body>
<>
{noApplicantsUnderThisTab && (
<ApplicantsBannerTab tab={searchParams.tab} />
)}
{shouldSearchBeDisplayed && (
<>
<ApplicantsSearchV2 />
<div className="md:hidden">
<ApplicantsFilterModalButton>
<ApplicantsFiltersV2
searchParams={searchParams}
params={params}
/>
</ApplicantsFilterModalButton>
</div>
</>
)}
<PageContentWithSide>
<PageContentWithSideLeft>
<div className="subheading">Filter by:</div>
<ApplicantsFiltersV2
searchParams={searchParams}
params={params}
/>
</PageContentWithSideLeft>
<PageContentWithSideMain>
{noApplicantsAfterSearch && (
<ApplicantsBannerSearch search={searchParams.search} />
)}
{hasTabApplicants && (
<Suspense
fallback={<Page.ContentLoader single />}
key={JSON.stringify(searchParams)}
>
<ErrorBoundary fallbackRender={Error}>
<ApplicantsList
isApplicantFilteringEnabled
params={params}
searchParams={searchParams}
/>
</ErrorBoundary>
</Suspense>
)}
</PageContentWithSideMain>
</PageContentWithSide>
</>
</Page.Body>
);
};
// infrastructure/features/Search/
export const SearchFilterSkills = ({ skills, skillsOptions }: Props) => {
const { getSkillUrl, setSkills } = useApplicantsSearchParams();
return (
<SearchFilterCard title="Skills">
<SearchFilterAutocomplete
getUrl={getSkillUrl}
options={skillsOptions}
values={skills}
setValues={setSkills}
placeholder="Search or select skills"
/>
</SearchFilterCard>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment