Skip to content

Commit

Permalink
feat: Update UI components and fix formatting issues
Browse files Browse the repository at this point in the history
  • Loading branch information
lewisblackburn committed Sep 4, 2024
1 parent 09edb43 commit 43ae802
Show file tree
Hide file tree
Showing 79 changed files with 3,010 additions and 596 deletions.
2 changes: 1 addition & 1 deletion app/components/banner.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Link } from '@remix-run/react'
import { useEffect, useState } from 'react'
import { Icon } from './ui/icon'
import { Link } from '@remix-run/react'

export default function Banner() {
// TODO: This should be set back to false if there is a new feature announcement
Expand Down
9 changes: 2 additions & 7 deletions app/components/chart/bar-chart.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import React from 'react'
import { BarChart as ReBarChart, XAxis, Bar, BarProps } from 'recharts'
import { BarChart as ReBarChart, XAxis, Bar } from 'recharts'
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from '../ui/card'
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '../ui/chart'
import { ChartContainer, ChartTooltip, ChartTooltipContent } from '../ui/chart'

interface ChartData {
[key: string]: string | number
Expand Down
14 changes: 3 additions & 11 deletions app/components/chart/pie.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import React from 'react'
import { Label, Pie, PieChart as RePieChart } from 'recharts'
import { Card, CardContent } from '../ui/card'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '../ui/card'
import {
ChartConfig,
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from '../ui/chart'
import { Label, Pie, PieChart as RePieChart } from 'recharts'
import { Icon } from '../ui/icon'

interface ChartData {
[key: string]: string | number
Expand Down
2 changes: 1 addition & 1 deletion app/components/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import { Form, Link } from '@remix-run/react'
import { type SVGProps } from 'react'
import { Icon } from './ui/icon'
import { Label } from './ui/label'
import { Input } from './ui/input'
import { Label } from './ui/label'

// Define the navigation types
type NavigationItem = {
Expand Down
21 changes: 21 additions & 0 deletions app/components/form/ErrorList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { FieldError } from './Field'

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

export function ErrorList({
id,
errors,
}: {
errors?: ListOfErrors
id?: string
}) {
const errorsToRender = errors?.filter(Boolean)
if (!errorsToRender?.length) return null
return (
<ul id={id} className="flex flex-col gap-1">
{errorsToRender.map((e) => (
<FieldError key={e}>{e}</FieldError>
))}
</ul>
)
}
17 changes: 17 additions & 0 deletions app/components/form/Field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { cn } from '#app/utils/misc.js'

export const Field = ({
children,
className,
}: {
children: React.ReactNode
className?: string
}) => {
return (
<div className={cn('flex flex-col gap-2 pb-6', className)}>{children}</div>
)
}

export const FieldError = ({ children }: { children: React.ReactNode }) => {
return <li className="text-[10px] text-foreground-destructive">{children}</li>
}
189 changes: 189 additions & 0 deletions app/components/form/MultiSelectField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { useInputControl } from '@conform-to/react'
import type * as PopoverPrimitive from '@radix-ui/react-popover'

import React, { useId } from 'react'
import { cn } from '#app/utils/misc.js'
import { Badge } from '../ui/badge'
import { Button } from '../ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '../ui/command'
import { Icon } from '../ui/icon'
import { Label } from '../ui/label'
import { Popover, PopoverTrigger, PopoverContent } from '../ui/popover'
import { ErrorList, type ListOfErrors } from './ErrorList'

export type OptionType = {
label: string
value: string
}

export type PopoverProps = Omit<
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Trigger>,
'type'
> & {
type?: string
}

export function MultiSelectField({
labelProps,
buttonProps,
options,
errors,
className,
}: {
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
buttonProps: PopoverProps & {
name: string
form: string
value?: string
}
options: OptionType[]
errors?: ListOfErrors
className?: string
}) {
const [open, setOpen] = React.useState(false)
const [selected, setSelected] = React.useState<string[]>(
// @ts-expect-error fix later
buttonProps.defaultValue ?? [],
)

const { key, name, ...selectProps } = buttonProps
const fallbackId = useId()
const input = useInputControl({
key,
name: buttonProps.name,
formId: buttonProps.form,
initialValue: buttonProps.defaultValue?.toString(),
})
const id = buttonProps.id ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined

const handleUnselect = (item: string) => {
setSelected(selected.filter((i) => i !== item))
}

return (
<div className={cn('flex flex-col space-y-2', className)}>
<input
name={buttonProps.name}
// Hack as readOnly prevents errors from being displayed
onChange={() => {}}
value={JSON.stringify(selected)}
className="hidden"
/>
<Label htmlFor={id} {...labelProps} />
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
{...selectProps}
id={id}
aria-invalid={errorId ? true : undefined}
aria-describedby={errorId}
onFocus={(event) => {
input.focus()
buttonProps.onFocus?.(event)
}}
onBlur={(event) => {
input.blur()
buttonProps.onBlur?.(event)
}}
type="button"
asChild
>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="min-w-[300px] justify-between whitespace-nowrap aria-[invalid]:border-input-invalid"
>
{selected.length < 1 &&
`Select ${labelProps.children?.toString().toLowerCase()}...`}
<div className="flex flex-wrap gap-1">
{selected.map((item) => (
<Badge
variant="secondary"
key={item}
className=""
onClick={() => handleUnselect(item)}
>
{options.find((option) => option.value === item)?.label ??
`Select ${labelProps.children
?.toString()
.toLowerCase()}...`}
<button
className="ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleUnselect(item)
}
}}
onMouseDown={(e) => {
e.preventDefault()
e.stopPropagation()
}}
onClick={() => handleUnselect(item)}
>
<Icon
name="cross-1"
className="h-3 w-3 text-muted-foreground hover:text-foreground"
/>
</button>
</Badge>
))}
</div>
<Icon name="caret-sort" className="h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput
placeholder={`Search ${labelProps.children
?.toString()
.toLowerCase()}...`}
className="h-9"
/>
<CommandList>
<CommandEmpty>
No {labelProps.children?.toString().toLowerCase()} found.
</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option.value}
onSelect={() => {
setSelected(
selected.includes(option.value)
? selected.filter((item) => item !== option.value)
: [...selected, option.value],
)
setOpen(true)
}}
>
<Icon
name="check"
className={cn(
'mr-2 h-4 w-4',
selected.includes(option.value)
? 'opacity-100'
: 'opacity-0',
)}
/>
{option.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<div className="px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
)
}
34 changes: 34 additions & 0 deletions app/components/form/RegularFIeld.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useId } from 'react'
import { Input } from '../ui/input'
import { Label } from '../ui/label'
import { ErrorList, type ListOfErrors } from './ErrorList'

export function RegularField({
labelProps,
inputProps,
errors,
className,
}: {
labelProps: React.LabelHTMLAttributes<HTMLLabelElement>
inputProps: React.InputHTMLAttributes<HTMLInputElement>
errors?: ListOfErrors
className?: string
}) {
const fallbackId = useId()
const id = inputProps.id ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined
return (
<div className={className}>
<Label htmlFor={id} {...labelProps} />
<Input
id={id}
aria-invalid={errorId ? true : undefined}
aria-describedby={errorId}
{...inputProps}
/>
<div className="min-h-[32px] px-4 pb-3 pt-1">
{errorId ? <ErrorList id={errorId} errors={errors} /> : null}
</div>
</div>
)
}
39 changes: 39 additions & 0 deletions app/components/form/conform/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
type FieldMetadata,
unstable_useControl as useControl,
} from '@conform-to/react'
import { useRef, type ElementRef } from 'react'
import { Checkbox } from '../../ui/checkbox'

export function CheckboxConform({
meta,
}: {
meta: FieldMetadata<string | boolean | undefined>
}) {
const checkboxRef = useRef<ElementRef<typeof Checkbox>>(null)
const control = useControl(meta)

return (
<>
<input
className="sr-only"
aria-hidden
ref={control.register}
name={meta.name}
tabIndex={-1}
defaultValue={meta.initialValue}
onFocus={() => checkboxRef.current?.focus()}
/>
<Checkbox
ref={checkboxRef}
id={meta.id}
checked={control.value === 'on'}
onCheckedChange={(checked) => {
control.change(checked ? 'on' : '')
}}
onBlur={control.blur}
className="focus:ring-2 focus:ring-stone-950 focus:ring-offset-2"
/>
</>
)
}
Loading

0 comments on commit 43ae802

Please sign in to comment.