mirror of
https://github.com/yangshun/tech-interview-handbook.git
synced 2025-07-27 12:12:03 +08:00
[portal][ui] add companies filter
This commit is contained in:
40
apps/portal/src/components/global/CompaniesTypeahead.tsx
Normal file
40
apps/portal/src/components/global/CompaniesTypeahead.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { TypeaheadOption } from '@tih/ui';
|
||||||
|
import { Typeahead } from '@tih/ui';
|
||||||
|
|
||||||
|
import { trpc } from '~/utils/trpc';
|
||||||
|
|
||||||
|
type Props = Readonly<{
|
||||||
|
disabled?: boolean;
|
||||||
|
onSelect: (option: TypeaheadOption) => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function CompaniesTypeahead({ disabled, onSelect }: Props) {
|
||||||
|
const [query, setQuery] = useState('');
|
||||||
|
const companies = trpc.useQuery([
|
||||||
|
'companies.list',
|
||||||
|
{
|
||||||
|
name: query,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { data } = companies;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typeahead
|
||||||
|
disabled={disabled}
|
||||||
|
label="Company"
|
||||||
|
noResultsMessage="No companies found"
|
||||||
|
nullable={true}
|
||||||
|
options={
|
||||||
|
data?.map(({ id, name }) => ({
|
||||||
|
id,
|
||||||
|
label: name,
|
||||||
|
value: id,
|
||||||
|
})) ?? []
|
||||||
|
}
|
||||||
|
onQueryChange={setQuery}
|
||||||
|
onSelect={onSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,11 +1,23 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { TypeaheadOption } from '@tih/ui';
|
||||||
|
|
||||||
|
import CompaniesTypeahead from '~/components/global/CompaniesTypeahead';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
|
const [selectedCompany, setSelectedCompany] =
|
||||||
|
useState<TypeaheadOption | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex-1 overflow-y-auto">
|
<main className="flex-1 overflow-y-auto">
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<h1 className="text-primary-600 text-center text-4xl font-bold">
|
<h1 className="text-primary-600 text-center text-4xl font-bold">
|
||||||
Homepage
|
Homepage
|
||||||
</h1>
|
</h1>
|
||||||
|
<CompaniesTypeahead
|
||||||
|
onSelect={(option) => setSelectedCompany(option)}
|
||||||
|
/>
|
||||||
|
<pre>{JSON.stringify(selectedCompany, null, 2)}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
23
apps/portal/src/server/router/companies-router.ts
Normal file
23
apps/portal/src/server/router/companies-router.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { createRouter } from './context';
|
||||||
|
|
||||||
|
export const companiesRouter = createRouter().query('list', {
|
||||||
|
input: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ ctx, input }) {
|
||||||
|
return await ctx.prisma.company.findMany({
|
||||||
|
orderBy: {
|
||||||
|
name: 'desc',
|
||||||
|
},
|
||||||
|
take: 10,
|
||||||
|
where: {
|
||||||
|
name: {
|
||||||
|
contains: input.name,
|
||||||
|
mode: 'insensitive',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
@ -1,5 +1,6 @@
|
|||||||
import superjson from 'superjson';
|
import superjson from 'superjson';
|
||||||
|
|
||||||
|
import { companiesRouter } from './companies-router';
|
||||||
import { createRouter } from './context';
|
import { createRouter } from './context';
|
||||||
import { protectedExampleRouter } from './protected-example-router';
|
import { protectedExampleRouter } from './protected-example-router';
|
||||||
import { questionsQuestionRouter } from './questions-question-router';
|
import { questionsQuestionRouter } from './questions-question-router';
|
||||||
@ -19,6 +20,7 @@ export const appRouter = createRouter()
|
|||||||
.merge('auth.', protectedExampleRouter)
|
.merge('auth.', protectedExampleRouter)
|
||||||
.merge('todos.', todosRouter)
|
.merge('todos.', todosRouter)
|
||||||
.merge('todos.user.', todosUserRouter)
|
.merge('todos.user.', todosUserRouter)
|
||||||
|
.merge('companies.', companiesRouter)
|
||||||
.merge('resumes.resume.', resumesRouter)
|
.merge('resumes.resume.', resumesRouter)
|
||||||
.merge('resumes.resume.user.', resumesResumeUserRouter)
|
.merge('resumes.resume.user.', resumesResumeUserRouter)
|
||||||
.merge('resumes.star.user.', resumesStarUserRouter)
|
.merge('resumes.star.user.', resumesStarUserRouter)
|
||||||
|
@ -14,6 +14,9 @@ export default {
|
|||||||
label: {
|
label: {
|
||||||
control: 'text',
|
control: 'text',
|
||||||
},
|
},
|
||||||
|
noResultsMessage: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
component: Typeahead,
|
component: Typeahead,
|
||||||
parameters: {
|
parameters: {
|
||||||
@ -62,9 +65,9 @@ export function Basic({
|
|||||||
isLabelHidden={isLabelHidden}
|
isLabelHidden={isLabelHidden}
|
||||||
label={label}
|
label={label}
|
||||||
options={filteredPeople}
|
options={filteredPeople}
|
||||||
selectedOption={selectedEntry}
|
value={selectedEntry}
|
||||||
onQueryChange={setQuery}
|
onQueryChange={setQuery}
|
||||||
onSelectOption={setSelectedEntry}
|
onSelect={setSelectedEntry}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -14,30 +14,51 @@ type Props = Readonly<{
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isLabelHidden?: boolean;
|
isLabelHidden?: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
|
noResultsMessage?: string;
|
||||||
|
nullable?: boolean;
|
||||||
onQueryChange: (
|
onQueryChange: (
|
||||||
value: string,
|
value: string,
|
||||||
event: React.ChangeEvent<HTMLInputElement>,
|
event: React.ChangeEvent<HTMLInputElement>,
|
||||||
) => void;
|
) => void;
|
||||||
onSelectOption: (option: TypeaheadOption) => void;
|
onSelect: (option: TypeaheadOption) => void;
|
||||||
options: ReadonlyArray<TypeaheadOption>;
|
options: ReadonlyArray<TypeaheadOption>;
|
||||||
selectedOption: TypeaheadOption;
|
value?: TypeaheadOption;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export default function Typeahead({
|
export default function Typeahead({
|
||||||
disabled = false,
|
disabled = false,
|
||||||
isLabelHidden,
|
isLabelHidden,
|
||||||
label,
|
label,
|
||||||
|
noResultsMessage = 'No results',
|
||||||
|
nullable = false,
|
||||||
options,
|
options,
|
||||||
onQueryChange,
|
onQueryChange,
|
||||||
selectedOption,
|
value,
|
||||||
onSelectOption,
|
onSelect,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
return (
|
return (
|
||||||
<Combobox
|
<Combobox
|
||||||
|
by="id"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={selectedOption}
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
onChange={onSelectOption}>
|
// @ts-ignore
|
||||||
|
multiple={false}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
nullable={nullable}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
value={value}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
if (newValue == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
onSelect(newValue as TypeaheadOption);
|
||||||
|
}}>
|
||||||
<Combobox.Label
|
<Combobox.Label
|
||||||
className={clsx(
|
className={clsx(
|
||||||
isLabelHidden
|
isLabelHidden
|
||||||
@ -54,10 +75,11 @@ export default function Typeahead({
|
|||||||
disabled && 'pointer-events-none select-none bg-slate-100',
|
disabled && 'pointer-events-none select-none bg-slate-100',
|
||||||
)}
|
)}
|
||||||
displayValue={(option) =>
|
displayValue={(option) =>
|
||||||
(option as unknown as TypeaheadOption).label
|
(option as unknown as TypeaheadOption)?.label
|
||||||
}
|
}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
!disabled && onQueryChange(event.target.value, event);
|
setQuery(event.target.value);
|
||||||
|
onQueryChange(event.target.value, event);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
|
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
@ -76,7 +98,7 @@ export default function Typeahead({
|
|||||||
<Combobox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
<Combobox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||||
{options.length === 0 && query !== '' ? (
|
{options.length === 0 && query !== '' ? (
|
||||||
<div className="relative cursor-default select-none py-2 px-4 text-slate-700">
|
<div className="relative cursor-default select-none py-2 px-4 text-slate-700">
|
||||||
Nothing found.
|
{noResultsMessage}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
options.map((option) => (
|
options.map((option) => (
|
||||||
|
Reference in New Issue
Block a user