diff --git a/src/app/sandbox/combobox/page.tsx b/src/app/sandbox/combobox/page.tsx new file mode 100644 index 00000000..273314bf --- /dev/null +++ b/src/app/sandbox/combobox/page.tsx @@ -0,0 +1,66 @@ +"use client"; +import { useState } from "react"; +import Combobox from "@/components/ui/combobox"; +import populateCragsAction from "./server-actions/populate-crags-action"; +import populateRoutesAction from "./server-actions/populate-routes-action"; + +function ComboboxPage() { + + async function populateCrags(text: string) { + if (text === "") { + return []; + } + const crags = await populateCragsAction(text); + return crags.map((crag) => ({ + value: crag.id, + name: crag.name, + })); + } + + async function populateRoutes(text: string) { + if (text === "") { + return []; + } + const routes = await populateRoutesAction(text); + return routes.map((route) => ({ + value: route.id, + name: `${route.name}, ${route.crag.name}`, + })); + } + + function handleChange(a) { + console.log("handleChange", a); + } + + return ( +
+

Combobox demo

+ +
+
Crag finder
+
+ +
+
+
+
Route finder
+
+ +
+
+
+ ); +} + +export default ComboboxPage; diff --git a/src/app/sandbox/combobox/server-actions/populate-crags-action.ts b/src/app/sandbox/combobox/server-actions/populate-crags-action.ts new file mode 100644 index 00000000..30f0bd35 --- /dev/null +++ b/src/app/sandbox/combobox/server-actions/populate-crags-action.ts @@ -0,0 +1,42 @@ +"use server"; + +import { gql } from "urql/core"; +import { + ComboboxPopulateCragsDocument, + Crag, + RouteDifficultyVotesDocument, +} from "@/graphql/generated"; +import urqlServer from "@/graphql/urql-server"; + +async function populateCragsAction( + query: string +): Promise { + const result = await urqlServer().query(ComboboxPopulateCragsDocument, { + input: { + query, + pageSize: 10, + orderBy: { field: "popularity", direction: "DESC" }, + }, + }); + + if (result.error) { + return Promise.reject(result.error); + } + + return result.data.searchCrags.items; +} + +export default populateCragsAction; + +gql` + query ComboboxPopulateCrags($input: SearchCragsInput!) { + searchCrags(input: $input) { + items { + __typename + id + name + slug + } + } + } +`; diff --git a/src/app/sandbox/combobox/server-actions/populate-routes-action.ts b/src/app/sandbox/combobox/server-actions/populate-routes-action.ts new file mode 100644 index 00000000..c3f0f028 --- /dev/null +++ b/src/app/sandbox/combobox/server-actions/populate-routes-action.ts @@ -0,0 +1,40 @@ +"use server"; + +import { gql } from "urql/core"; +import { ComboboxPopulateCragsDocument, ComboboxPopulateRoutesDocument, Route } from "@/graphql/generated"; +import urqlServer from "@/graphql/urql-server"; + +async function populateRoutesAction(query: string): Promise { + const result = await urqlServer().query(ComboboxPopulateRoutesDocument, { + input: { + query, + pageSize: 10, + orderBy: { field: "popularity", direction: "DESC" }, + }, + }); + + if (result.error) { + return Promise.reject(result.error); + } + + return result.data.searchRoutes.items; +} + +export default populateRoutesAction; + +gql` + query ComboboxPopulateRoutes($input: SearchRoutesInput!) { + searchRoutes(input: $input) { + items { + __typename + id + name + slug + crag { + id + name + } + } + } + } +`; diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx new file mode 100644 index 00000000..541f941f --- /dev/null +++ b/src/components/ui/combobox.tsx @@ -0,0 +1,121 @@ +import useForwardedRef from "@/hooks/useForwardedRef"; +import { + Combobox as HUICombobox, + ComboboxInput, + ComboboxOption, + ComboboxOptions, + Field, + ComboboxButton, + Label, +} from "@headlessui/react"; +import { ForwardedRef, forwardRef, useEffect, useState } from "react"; +import IconSearch from "./icons/search"; + +type TComboboxProps = { + name?: string; + value?: TComboboxValue | null; + type?: string; + onChange: (value: string | null) => void; + label?: string; + placeholder?: string; + description?: string; + errorMessage?: string; + disabled?: boolean; + onBlur?: () => void; + populate: (text: string) => Promise; +}; + +type TComboboxValue = { + value: string; + name: string; +}; + +const Combobox = forwardRef(function Combobox( + { + value, + onChange, + label, + errorMessage, + disabled, + populate, + }: TComboboxProps, + forwardedRef: ForwardedRef +) { + const inputRef = useForwardedRef(forwardedRef); + + const focusInput = () => { + inputRef.current?.focus(); + }; + + const [selectedValue, setSelectedValue] = useState(value); + const [query, setQuery] = useState(""); + + const [options, setOptions] = useState([]); + + useEffect(() => { + populate(query).then((result) => { + setOptions(result); + }); + }, [query, populate]); + + function handleChange(newValue: TComboboxValue) { + setSelectedValue(newValue); + onChange(newValue?.value || null); + } + + return ( + + {label && } + setQuery("")} + > +
+ value?.name} + onChange={(event) => setQuery(event.target.value)} + className="flex-1 outline-none min-w-0 rounded-lg w-full py-2 placeholder:text-neutral-400 pl-4" + autoComplete="off" + /> + +
+ +
+
+
+ {options.length > 0 && ( + + {options.map((option: TComboboxValue) => ( + + {option.name} + + ))} + + )} +
+
+ ); +}); + +export default Combobox; diff --git a/src/graphql/generated.ts b/src/graphql/generated.ts index bfdb9a3a..ed96893e 100644 --- a/src/graphql/generated.ts +++ b/src/graphql/generated.ts @@ -817,12 +817,24 @@ export type PaginatedComments = { meta: PaginationMeta; }; +export type PaginatedCrags = { + __typename?: 'PaginatedCrags'; + items: Array; + meta: PaginationMeta; +}; + export type PaginatedDifficultyVotes = { __typename?: 'PaginatedDifficultyVotes'; items: Array; meta: PaginationMeta; }; +export type PaginatedRoutes = { + __typename?: 'PaginatedRoutes'; + items: Array; + meta: PaginationMeta; +}; + export type PaginationMeta = { __typename?: 'PaginationMeta'; itemCount: Scalars['Float']['output']; @@ -935,6 +947,8 @@ export type Query = { routeBySlug: Route; routesTouches: RoutesTouches; search: SearchResults; + searchCrags: PaginatedCrags; + searchRoutes: PaginatedRoutes; sector: Sector; starRatingVotes: Array; users: Array; @@ -1107,6 +1121,16 @@ export type QuerySearchArgs = { }; +export type QuerySearchCragsArgs = { + input: SearchCragsInput; +}; + + +export type QuerySearchRoutesArgs = { + input: SearchRoutesInput; +}; + + export type QuerySectorArgs = { id: Scalars['String']['input']; }; @@ -1211,6 +1235,13 @@ export type RoutesTouches = { tried: Array; }; +export type SearchCragsInput = { + orderBy?: InputMaybe; + pageNumber?: InputMaybe; + pageSize?: InputMaybe; + query: Scalars['String']['input']; +}; + export type SearchResults = { __typename?: 'SearchResults'; comments?: Maybe>; @@ -1220,6 +1251,13 @@ export type SearchResults = { users?: Maybe>; }; +export type SearchRoutesInput = { + orderBy?: InputMaybe; + pageNumber?: InputMaybe; + pageSize?: InputMaybe; + query: Scalars['String']['input']; +}; + export enum Season { Autumn = 'autumn', Spring = 'spring', @@ -1411,6 +1449,8 @@ export const MyCragSummaryDocument = {"kind":"Document","definitions":[{"kind":" export const AllCragsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AllCrags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"crags"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"type"},"value":{"kind":"StringValue","value":"sport","block":false}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"country"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"Field","name":{"kind":"Name","value":"area"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"country"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"orientations"}},{"kind":"Field","name":{"kind":"Name","value":"minDifficulty"}},{"kind":"Field","name":{"kind":"Name","value":"maxDifficulty"}},{"kind":"Field","name":{"kind":"Name","value":"seasons"}},{"kind":"Field","name":{"kind":"Name","value":"rainproof"}},{"kind":"Field","name":{"kind":"Name","value":"wallAngles"}},{"kind":"Field","name":{"kind":"Name","value":"approachTime"}},{"kind":"Field","name":{"kind":"Name","value":"nrRoutesByGrade"}},{"kind":"Field","name":{"kind":"Name","value":"hasSport"}},{"kind":"Field","name":{"kind":"Name","value":"hasBoulder"}},{"kind":"Field","name":{"kind":"Name","value":"hasMultipitch"}},{"kind":"Field","name":{"kind":"Name","value":"nrRoutes"}}]}}]}}]} as unknown as DocumentNode; export const AllCountriesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AllCountries"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"countries"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"nrCrags"}},{"kind":"Field","name":{"kind":"Name","value":"areas"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"nrCrags"}}]}}]}}]}}]} as unknown as DocumentNode; export const LoginDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Login"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"password"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"login"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"password"},"value":{"kind":"Variable","name":{"kind":"Name","value":"password"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"firstname"}},{"kind":"Field","name":{"kind":"Name","value":"lastname"}},{"kind":"Field","name":{"kind":"Name","value":"gender"}},{"kind":"Field","name":{"kind":"Name","value":"roles"}}]}}]}}]}}]} as unknown as DocumentNode; +export const ComboboxPopulateCragsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ComboboxPopulateCrags"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SearchCragsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"searchCrags"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]} as unknown as DocumentNode; +export const ComboboxPopulateRoutesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ComboboxPopulateRoutes"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SearchRoutesInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"searchRoutes"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"crag"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const CragDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Crag"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"crag"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cragBySlug"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"crag"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"sectors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"routes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"difficulty"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"defaultGradingSystem"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const CragActivitiesByMonthDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CragActivitiesByMonth"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"crag"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cragBySlug"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"crag"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"activityByMonth"}}]}}]}}]} as unknown as DocumentNode; export const CreateActivityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateActivity"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateActivityInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"routes"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateActivityRouteInput"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createActivity"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}},{"kind":"Argument","name":{"kind":"Name","value":"routes"},"value":{"kind":"Variable","name":{"kind":"Name","value":"routes"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; @@ -1429,6 +1469,8 @@ export const namedOperations = { MyCragSummary: 'MyCragSummary', AllCrags: 'AllCrags', AllCountries: 'AllCountries', + ComboboxPopulateCrags: 'ComboboxPopulateCrags', + ComboboxPopulateRoutes: 'ComboboxPopulateRoutes', Crag: 'Crag', CragActivitiesByMonth: 'CragActivitiesByMonth', DryRunCreateActivity: 'DryRunCreateActivity', @@ -1551,6 +1593,20 @@ export type LoginMutationVariables = Exact<{ export type LoginMutation = { __typename?: 'Mutation', login: { __typename?: 'LoginResponse', token: string, user: { __typename?: 'User', id: string, email?: string | null, fullName: string, firstname: string, lastname: string, gender?: string | null, roles: Array } } }; +export type ComboboxPopulateCragsQueryVariables = Exact<{ + input: SearchCragsInput; +}>; + + +export type ComboboxPopulateCragsQuery = { __typename?: 'Query', searchCrags: { __typename?: 'PaginatedCrags', items: Array<{ __typename: 'Crag', id: string, name: string, slug: string }> } }; + +export type ComboboxPopulateRoutesQueryVariables = Exact<{ + input: SearchRoutesInput; +}>; + + +export type ComboboxPopulateRoutesQuery = { __typename?: 'Query', searchRoutes: { __typename?: 'PaginatedRoutes', items: Array<{ __typename: 'Route', id: string, name: string, slug: string, crag: { __typename?: 'Crag', id: string, name: string } }> } }; + export type CragQueryVariables = Exact<{ crag: Scalars['String']['input']; }>;