Skip to content

Commit

Permalink
feat: change combobox to make it more maintainable (#166)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jshen123 authored Apr 16, 2023
1 parent fb956f9 commit a3e0e5f
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 7 deletions.
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"dependencies": {
"@formkit/auto-animate": "^1.0.0-beta.6",
"@headlessui/react": "^1.7.14",
"@next-auth/prisma-adapter": "^1.0.5",
"@prisma/client": "^4.9.0",
"@radix-ui/react-toast": "^1.1.3",
Expand Down
68 changes: 68 additions & 0 deletions src/components/Combobox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useState } from "react";
import { Combobox as ComboboxPrimitive } from "@headlessui/react";
import { FaChevronDown } from "react-icons/fa";
import clsx from "clsx";

interface ComboboxProps {
value: string;
options: string[];
left?: boolean;
disabled?: boolean;
onChange: (value: string) => void;
}

const Combobox = ({
options,
value,
left = true,
disabled,
onChange,
}: ComboboxProps) => {
const [query, setQuery] = useState("");
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (
event.target instanceof HTMLInputElement &&
typeof event.target.value === "string"
) {
setQuery(event.target.value);
}
};

const filteredOptions =
query === ""
? options
: options.filter((opt) => {
return opt.toLowerCase().includes(query.toLowerCase());
});

return (
<ComboboxPrimitive value={value} onChange={onChange} disabled={disabled}>
<div className="relative w-full">
<ComboboxPrimitive.Input
onChange={handleInputChange}
className={clsx(
"border:black delay-50 sm: flex w-full items-center justify-between rounded-xl border-[2px] border-white/10 bg-transparent px-2 py-2 text-sm tracking-wider outline-0 transition-all hover:border-[#1E88E5]/40 focus:border-[#1E88E5] sm:py-3 md:text-lg",
disabled && " cursor-not-allowed hover:border-white/10",
left && "md:rounded-l-none"
)}
/>
<ComboboxPrimitive.Button className="absolute inset-y-0 right-0 flex items-center pr-4">
<FaChevronDown className="h-5 w-5 text-gray-400" aria-hidden="true" />
</ComboboxPrimitive.Button>
<ComboboxPrimitive.Options className="absolute right-0 top-full z-20 mt-1 max-h-48 w-full overflow-auto rounded-xl border-[2px] border-white/10 bg-[#3a3a3a] tracking-wider shadow-xl outline-0 transition-all ">
{filteredOptions.map((opt) => (
<ComboboxPrimitive.Option
key={opt}
value={opt}
className="cursor-pointer px-2 py-2 font-mono text-sm text-white/75 hover:bg-blue-500 sm:py-3 md:text-lg"
>
{opt}
</ComboboxPrimitive.Option>
))}
</ComboboxPrimitive.Options>
</div>
</ComboboxPrimitive>
);
};

export default Combobox;
39 changes: 36 additions & 3 deletions src/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,51 @@
import React from "react";
import Label from "./Label";
import clsx from "clsx";
import Combobox from "./Combobox";
import isArrayOfType from "../utils/helpers";

interface InputProps {
left?: React.ReactNode;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
placeholder?: string;
disabled?: boolean;
setValue?: (value: string) => void;
type?: string;
attributes?: { [key: string]: string | number | string[] }; // attributes specific to input type
}

const Input = ({
placeholder,
left,
value,
type,
onChange,
setValue,
disabled,
attributes,
}: InputProps) => {
return (
<div className="items-left z-10 flex w-full flex-col rounded-xl bg-[#3a3a3a] font-mono text-lg text-white/75 shadow-xl md:flex-row md:items-center">
{left && <Label left={left} />}
const isTypeCombobox = () => {
return type === "combobox";
};

let inputElement;
const options = attributes?.options;
if (
isTypeCombobox() &&
isArrayOfType(options, "string") &&
setValue !== undefined
) {
inputElement = (
<Combobox
value={value}
options={options}
disabled={disabled}
onChange={setValue}
/>
);
} else {
inputElement = (
<input
className={clsx(
"border:black delay-50 w-full rounded-xl border-[2px] border-white/10 bg-transparent px-2 py-2 text-sm tracking-wider outline-0 transition-all placeholder:text-white/20 hover:border-[#1E88E5]/40 focus:border-[#1E88E5] sm:py-3 md:text-lg",
Expand All @@ -32,6 +58,13 @@ const Input = ({
onChange={onChange}
disabled={disabled}
/>
);
}

return (
<div className="items-left z-10 flex w-full flex-col rounded-xl bg-[#3a3a3a] font-mono text-lg text-white/75 shadow-xl md:flex-row md:items-center">
{left && <Label left={left} />}
{inputElement}
</div>
);
};
Expand Down
10 changes: 6 additions & 4 deletions src/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import Button from "./Button";
import { FaKey, FaMicrochip, FaBell } from "react-icons/fa";
import { FaKey, FaMicrochip } from "react-icons/fa";
import Dialog from "./Dialog";
import Input from "./Input";
import Dropdown from "./Dropdown";
Expand Down Expand Up @@ -49,16 +49,18 @@ export default function SettingsDialog({
<p>To use GPT-4, your API Key needs to have the correct access.</p>
<br />
<div className="text-md relative flex-auto p-2 leading-relaxed">
<Dropdown
<Input
left={
<>
<FaMicrochip />
<span className="ml-2">Model:</span>
</>
}
type="combobox"
value={customModelName}
options={GPT_MODEL_NAMES}
setCustomModelName={setCustomModelName}
onChange={(e) => null}
setValue={setCustomModelName}
attributes={{ options: GPT_MODEL_NAMES }}
/>
<br className="hidden md:inline" />
<Input
Expand Down
20 changes: 20 additions & 0 deletions src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
type Constructor<T> = new (...args: unknown[]) => T;

/* Check whether array is of the specified type */
const isArrayOfType = <T>(
arr: unknown[] | unknown,
type: Constructor<T> | string
): arr is T[] => {
return (
Array.isArray(arr) &&
arr.every((item): item is T => {
if (typeof type === "string") {
return typeof item === type;
} else {
return item instanceof type;
}
})
);
};

export default isArrayOfType;

1 comment on commit a3e0e5f

@vercel
Copy link

@vercel vercel bot commented on a3e0e5f Apr 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.