Skip to content

Commit

Permalink
feat: Add header name suggestions (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
dan-lee authored Jan 29, 2024
1 parent 3101542 commit ab59fd4
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 34 deletions.
83 changes: 70 additions & 13 deletions www/components/HeaderRow.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { useState } from "react";
import { useMemo, useRef, useState } from "react";
import { useCombobox } from "downshift";
import { matchSorter } from "match-sorter";
import cn from "classnames";

import { Header } from "./HeadersList";
import Input from "@/components/Input";
import TrashIcon from "@/components/TrashIcon";
import { CommonHeaders } from "@/utils/commonHeaders";
import ArrowIcon from "@/components/ArrowIcon";
import Highlight from "@/components/Highlight";

interface HeaderRowProps {
header: Header;
Expand All @@ -20,6 +27,7 @@ const HeaderRow = ({
}: HeaderRowProps) => {
const [keyError, setKeyError] = useState<string | undefined>();
const [valueError, setValueError] = useState<string | undefined>();
const valueRef = useRef<HTMLInputElement | null>(null);

const validateKey = (newKey: string) => {
// Empty key validation
Expand Down Expand Up @@ -61,11 +69,6 @@ const HeaderRow = ({
return true;
};

const onChangeKey = (e: React.ChangeEvent<HTMLInputElement>) => {
const isValid = validateKey(e.target.value);
onChange({ ...header, key: e.target.value, hasError: !isValid });
};

const onChangeValue = (e: React.ChangeEvent<HTMLInputElement>) => {
const isValid = validateValue(e.target.value);
onChange({ ...header, value: e.target.value, hasError: !isValid });
Expand All @@ -74,16 +77,69 @@ const HeaderRow = ({
const onClickDelete = () => {
onDelete?.(header.id);
};
const [inputValue, setInputValue] = useState(header.key);

const filteredHeaders = useMemo(
() => matchSorter(CommonHeaders, inputValue, {}),
[inputValue],
);

const {
isOpen,
getMenuProps,
getInputProps,
getItemProps,
getToggleButtonProps,
highlightedIndex,
} = useCombobox({
items: filteredHeaders,
initialInputValue: inputValue,
onSelectedItemChange: ({ selectedItem }) => {
if (!selectedItem) return;
onChange({ ...header, key: selectedItem });
setInputValue(selectedItem);
valueRef.current?.focus();
valueRef.current?.select();
},
onInputValueChange: ({ inputValue }) => {
if (!inputValue) return;
onChange({ ...header, key: inputValue });
setInputValue(inputValue);
},
});

return (
<div className="flex align-top items-start gap-2 flex-wrap sm:flex-nowrap">
<div className="w-full flex flex-col">
<Input
value={header.key}
placeholder="Key"
title={"Header key"}
onChange={onChangeKey}
/>
<div className="w-full flex flex-col relative">
<Input placeholder="Key" title="Header key" {...getInputProps()} />
<button
type="button"
className="absolute right-1 text-slate-600 top-0 bottom-0 flex items-center"
{...getToggleButtonProps()}
>
<ArrowIcon className="rotate-90" />
</button>
<ul
className={cn(
!isOpen && "hidden",
"absolute z-10 top-full max-h-[200px] overflow-y-auto translate-y-1 w-full bg-slate-800 rounded-md",
)}
{...getMenuProps()}
>
{isOpen &&
filteredHeaders.map((item, index) => (
<li
className={cn(
"p-2 cursor-pointer font-mono text-xs",
highlightedIndex === index && "bg-slate-700",
)}
key={`${item}-${index}`}
{...getItemProps({ item, index })}
>
<Highlight highlight={inputValue} text={item} />
</li>
))}
</ul>
{keyError ? (
<p className="mt-2 text-sm text-red-600">&nbsp;{keyError}</p>
) : null}
Expand All @@ -93,6 +149,7 @@ const HeaderRow = ({
value={header.value}
onChange={onChangeValue}
placeholder="Value"
ref={valueRef}
/>
{valueError ? (
<p className="mt-2 text-sm text-red-600">&nbsp;{valueError}</p>
Expand Down
31 changes: 31 additions & 0 deletions www/components/Highlight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from "react";

const escapeRegExp = (str: string): string =>
str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

const Highlight = ({
highlight,
text,
}: {
highlight: string;
text: string;
}) => {
if (!highlight.trim()) {
return text;
}

const highlightChars = highlight.split("").map(escapeRegExp).join("|");
const regex = new RegExp(`(${highlightChars})`, "gi");

return text.split(regex).map((part, index) =>
regex.test(part) ? (
<mark className="font-bold bg-zuplo-primary/50 text-white" key={index}>
{part}
</mark>
) : (
part
),
);
};

export default Highlight;
24 changes: 13 additions & 11 deletions www/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
import React from "react";
import cn from "classnames";

const Input = ({
value,
onChange,
className,
textarea,
...props
}: { textarea?: boolean } & React.InputHTMLAttributes<
HTMLInputElement | HTMLTextAreaElement
>) => {
type InputProps = {
textarea?: boolean;
} & React.InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement>;

const Input = React.forwardRef<
HTMLInputElement | HTMLTextAreaElement,
InputProps
>(({ value, onChange, className, textarea, ...props }, ref) => {
const Comp = textarea ? "textarea" : "input";
return (
<Comp
ref={ref as any}
value={value}
onChange={onChange}
className={cn(
"bg-transparent border font-mono p-1 px-2 border-slate-700 flex-grow rounded",
"focus:ring-4 focus:outline-none focus:ring-slate-700/50",
"focus:ring-4 focus:outline-none focus:ring-slate-700/50 hover:bg-black/25",
className,
)}
{...props}
/>
);
};
});

Input.displayName = "Input";

export default Input;
56 changes: 46 additions & 10 deletions www/package-lock.json

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

2 changes: 2 additions & 0 deletions www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
},
"dependencies": {
"classnames": "^2.3.2",
"downshift": "8.3.1",
"match-sorter": "6.3.3",
"next": "14.0.1",
"posthog-js": "^1.88.1",
"react": "^18",
Expand Down
69 changes: 69 additions & 0 deletions www/utils/commonHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
export const CommonHeaders = [
"Accept",
"Accept-Charset",
"Accept-Encoding",
"Accept-Language",
"Accept-Patch",
"Accept-Ranges",
"Access-Control-Allow-Credentials",
"Access-Control-Allow-Headers",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Origin",
"Access-Control-Expose-Headers",
"Access-Control-Max-Age",
"Age",
"Authorization",
"Cache-Control",
"Connection",
"Content-Disposition",
"Content-Encoding",
"Content-Language",
"Content-Length",
"Content-Location",
"Content-Range",
"Content-Security-Policy",
"Content-Transfer-Encoding",
"Content-Type",
"Date",
"ETag",
"Expect",
"Expires",
"Forwarded",
"If-Match",
"If-Modified-Since",
"If-None-Match",
"If-Unmodified-Since",
"Keep-Alive",
"Last-Modified",
"Link",
"Location",
"Origin",
"Pragma",
"Proxy-Authenticate",
"Range",
"Referer",
"Referrer-Policy",
"Retry-After",
"Server",
"Set-Cookie",
"Strict-Transport-Security",
"TE",
"Trailer",
"Transfer-Encoding",
"Upgrade",
"User-Agent",
"Vary",
"Via",
"WWW-Authenticate",
"Warning",
"X-Content-Type-Options",
"X-DNS-Prefetch-Control",
"X-Forwarded-For",
"X-Frame-Options",
"X-Powered-By",
"X-Requested-With",
"X-Robots-Tag",
"X-UA-Compatible",
"X-XSS-Protection",
"X-XSS-Protection",
];

0 comments on commit ab59fd4

Please sign in to comment.