Skip to content

Commit aa53733

Browse files
committed
02/04(wip): solution
1 parent 380a034 commit aa53733

File tree

16 files changed

+1102
-52
lines changed

16 files changed

+1102
-52
lines changed

exercises/02.test-setup/03.solution.authentication/tests/test-extend.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ interface Fixtures {
1313
navigate: <T extends keyof Register['pages']>(
1414
...args: Parameters<typeof href<T>>
1515
) => Promise<void>
16-
1716
authenticate: AuthenticateFunction<[typeof user]>
1817
}
1918

exercises/02.test-setup/04.solution.api-mocking/app/components/forms.tsx

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useInputControl } from '@conform-to/react'
22
import { REGEXP_ONLY_DIGITS_AND_CHARS, type OTPInputProps } from 'input-otp'
3-
import React, { useId } from 'react'
3+
import React, { useEffect, useId, useState } from 'react'
44
import { Checkbox, type CheckboxProps } from './ui/checkbox.tsx'
55
import {
66
InputOTP,
@@ -11,6 +11,8 @@ import {
1111
import { Input } from './ui/input.tsx'
1212
import { Label } from './ui/label.tsx'
1313
import { Textarea } from './ui/textarea.tsx'
14+
import { cn } from '#app/utils/misc.tsx'
15+
import { Command, CommandItem, CommandList } from './ui/command.tsx'
1416

1517
export type ListOfErrors = Array<string | null | undefined> | null | undefined
1618

@@ -200,3 +202,86 @@ export function CheckboxField({
200202
</div>
201203
)
202204
}
205+
206+
export function ComboboxField({
207+
labelProps,
208+
inputProps,
209+
errors,
210+
options,
211+
className,
212+
}: {
213+
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
214+
inputProps: React.TextareaHTMLAttributes<HTMLInputElement> & { key: any }
215+
errors?: ListOfErrors
216+
options: Array<{ label: string; value: string }>
217+
className?: string
218+
}) {
219+
const fallbackId = useId()
220+
const id = inputProps.id ?? fallbackId
221+
const control = useInputControl({
222+
key: inputProps.key,
223+
name: inputProps.name!,
224+
formId: inputProps.form!,
225+
})
226+
const errorId = errors?.length ? `${id}-error` : undefined
227+
228+
const [query, setQuery] = useState<string>()
229+
const [filtered, setFiltered] = useState<
230+
Array<{ label: string; value: string }>
231+
>([])
232+
233+
useEffect(() => {
234+
setFiltered(
235+
query && query.length > 2
236+
? options.filter((option) => {
237+
return (
238+
option.value.toLowerCase().includes(query.toLowerCase()) ||
239+
option.label.toLowerCase().includes(query.toLowerCase())
240+
)
241+
})
242+
: [],
243+
)
244+
}, [query, options])
245+
246+
return (
247+
<div className={cn('relative', className)}>
248+
<Label htmlFor={id} {...labelProps} />
249+
<Input
250+
{...inputProps}
251+
aria-invalid={errorId ? true : undefined}
252+
aria-describedby={errorId}
253+
autoComplete="off"
254+
value={query || ''}
255+
onChange={(event) => {
256+
setQuery(event.target.value)
257+
}}
258+
/>
259+
260+
{filtered.length > 0 && (
261+
<div className="bg-popover text-popover-foreground border-muted-foreground/60 absolute z-10 mt-1 w-full rounded-md border shadow">
262+
<Command>
263+
<CommandList>
264+
{filtered.map((option) => (
265+
<CommandItem
266+
key={option.value}
267+
value={option.value}
268+
onSelect={() => {
269+
control.change(option.value)
270+
setQuery(option.value)
271+
setFiltered([])
272+
}}
273+
>
274+
{option.label}
275+
</CommandItem>
276+
))}
277+
</CommandList>
278+
</Command>
279+
</div>
280+
)}
281+
282+
<div className="px-4 pt-1 pb-3">
283+
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
284+
</div>
285+
</div>
286+
)
287+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {
2+
Command,
3+
CommandEmpty,
4+
CommandGroup,
5+
CommandInput,
6+
CommandItem,
7+
CommandList,
8+
} from '#app/components/ui/command'
9+
10+
export function Combobox({
11+
id,
12+
name,
13+
placeholder,
14+
className,
15+
items,
16+
value,
17+
onChange,
18+
}: Omit<React.ComponentProps<typeof CommandInput>, 'value'> & {
19+
items: Array<{ label: string; value: string }>
20+
value: string
21+
onChange: (nextValue: string) => void
22+
}) {
23+
return (
24+
<Command className={className}>
25+
<CommandInput
26+
id={id}
27+
name={name}
28+
placeholder={placeholder}
29+
className="h-9"
30+
value={value}
31+
/>
32+
<CommandList>
33+
<CommandEmpty>No framework found.</CommandEmpty>
34+
<CommandGroup>
35+
{items.map((item) => (
36+
<CommandItem
37+
key={item.value}
38+
value={item.value}
39+
onSelect={(nextValue) => {
40+
onChange?.(nextValue === value ? '' : nextValue)
41+
}}
42+
>
43+
{item.label}
44+
</CommandItem>
45+
))}
46+
</CommandGroup>
47+
</CommandList>
48+
</Command>
49+
)
50+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import * as React from 'react'
2+
import { Command as CommandPrimitive } from 'cmdk'
3+
4+
import {
5+
Dialog,
6+
DialogContent,
7+
DialogDescription,
8+
DialogHeader,
9+
DialogTitle,
10+
} from '#app/components/ui/dialog'
11+
import { cn } from '#app/utils/misc'
12+
import { Icon } from './icon'
13+
14+
function Command({
15+
className,
16+
...props
17+
}: React.ComponentProps<typeof CommandPrimitive>) {
18+
return (
19+
<CommandPrimitive
20+
data-slot="command"
21+
className={cn(
22+
'bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md',
23+
className,
24+
)}
25+
{...props}
26+
/>
27+
)
28+
}
29+
30+
function CommandDialog({
31+
title = 'Command Palette',
32+
description = 'Search for a command to run...',
33+
children,
34+
className,
35+
showCloseButton = true,
36+
...props
37+
}: React.ComponentProps<typeof Dialog> & {
38+
title?: string
39+
description?: string
40+
className?: string
41+
showCloseButton?: boolean
42+
}) {
43+
return (
44+
<Dialog {...props}>
45+
<DialogHeader className="sr-only">
46+
<DialogTitle>{title}</DialogTitle>
47+
<DialogDescription>{description}</DialogDescription>
48+
</DialogHeader>
49+
<DialogContent
50+
className={cn('overflow-hidden p-0', className)}
51+
showCloseButton={showCloseButton}
52+
>
53+
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
54+
{children}
55+
</Command>
56+
</DialogContent>
57+
</Dialog>
58+
)
59+
}
60+
61+
function CommandInput({
62+
className,
63+
...props
64+
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
65+
return (
66+
<div
67+
data-slot="command-input-wrapper"
68+
className="border-b-muted flex h-9 items-center gap-2 border-b px-3"
69+
>
70+
<Icon name="magnifying-glass" className="size-4 shrink-0 opacity-50" />
71+
<CommandPrimitive.Input
72+
data-slot="command-input"
73+
className={cn(
74+
'placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
75+
className,
76+
)}
77+
{...props}
78+
/>
79+
</div>
80+
)
81+
}
82+
83+
function CommandList({
84+
className,
85+
...props
86+
}: React.ComponentProps<typeof CommandPrimitive.List>) {
87+
return (
88+
<CommandPrimitive.List
89+
data-slot="command-list"
90+
className={cn(
91+
'max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto',
92+
className,
93+
)}
94+
{...props}
95+
/>
96+
)
97+
}
98+
99+
function CommandEmpty({
100+
...props
101+
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
102+
return (
103+
<CommandPrimitive.Empty
104+
data-slot="command-empty"
105+
className="py-6 text-center text-sm"
106+
{...props}
107+
/>
108+
)
109+
}
110+
111+
function CommandGroup({
112+
className,
113+
...props
114+
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
115+
return (
116+
<CommandPrimitive.Group
117+
data-slot="command-group"
118+
className={cn(
119+
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
120+
className,
121+
)}
122+
{...props}
123+
/>
124+
)
125+
}
126+
127+
function CommandSeparator({
128+
className,
129+
...props
130+
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
131+
return (
132+
<CommandPrimitive.Separator
133+
data-slot="command-separator"
134+
className={cn('bg-border -mx-1 h-px', className)}
135+
{...props}
136+
/>
137+
)
138+
}
139+
140+
function CommandItem({
141+
className,
142+
...props
143+
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
144+
return (
145+
<CommandPrimitive.Item
146+
data-slot="command-item"
147+
className={cn(
148+
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
149+
className,
150+
)}
151+
{...props}
152+
/>
153+
)
154+
}
155+
156+
function CommandShortcut({
157+
className,
158+
...props
159+
}: React.ComponentProps<'span'>) {
160+
return (
161+
<span
162+
data-slot="command-shortcut"
163+
className={cn(
164+
'text-muted-foreground ml-auto text-xs tracking-widest',
165+
className,
166+
)}
167+
{...props}
168+
/>
169+
)
170+
}
171+
172+
export {
173+
Command,
174+
CommandDialog,
175+
CommandInput,
176+
CommandList,
177+
CommandEmpty,
178+
CommandGroup,
179+
CommandItem,
180+
CommandShortcut,
181+
CommandSeparator,
182+
}

0 commit comments

Comments
 (0)