Skip to content

Commit

Permalink
semi working search 3/4
Browse files Browse the repository at this point in the history
  • Loading branch information
sphinxrave committed Oct 24, 2023
1 parent 0870b2e commit bacdb36
Show file tree
Hide file tree
Showing 11 changed files with 354 additions and 175 deletions.
8 changes: 4 additions & 4 deletions packages/react/.lintstagedrc.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
{
"*.ts": [
"prettier --write",
"eslint"
"eslint --fix"
],
"*.tsx": [
"prettier --write",
"eslint"
"eslint --fix"
],
"*.js": [
"prettier --write",
"eslint"
"eslint --fix"
],
"*.jsx": [
"prettier --write",
"eslint"
"eslint --fix"
],
"*.html": [
"eslint",
Expand Down
8 changes: 4 additions & 4 deletions packages/react/src/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import { useAtom, useAtomValue } from "jotai";
import { useSetAtom } from "jotai/react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { SearchBar } from "./searchbar/SearchBar";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "@/shadcn/ui/dropdown-menu";
import { useAuth } from "@/hooks/useAuth";
import { SearchBar } from "./searchbar/SearchBar";

interface HeaderProps
extends React.DetailedHTMLProps<
Expand All @@ -31,7 +31,7 @@ export function Header({ id }: HeaderProps) {
const { logout } = useAuth();

return (
<header id={id} className="z-40 flex items-center gap-4 bg-base-2 px-2">
<header id={id} className="bg-base-2 z-40 flex items-center gap-4 px-2">
<Button
size="icon"
variant="ghost"
Expand All @@ -45,15 +45,15 @@ export function Header({ id }: HeaderProps) {
<Button
size="icon"
variant="ghost"
className="-ml-3 p-0 text-base-9"
className="text-base-9 -ml-3 p-0"
onClick={() => toggle(!dark)}
>
<div className="i-heroicons:magnifying-glass h-full text-xl" />
</Button>
<Button
size="icon"
variant="ghost"
className="p-0 text-base-9"
className="text-base-9 p-0"
onClick={() => toggle(!dark)}
>
<div className="i-heroicons:sun-20-solid h-full text-xl " />
Expand Down
31 changes: 31 additions & 0 deletions packages/react/src/components/header/searchbar/QueryBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from "react";
import { X } from "lucide-react";
import { Badge } from "@/shadcn/ui/badge";
import { QueryItem } from "./types";
import { PrimitiveAtom, useAtomValue, useSetAtom } from "jotai";
import { splitQueryAtom } from "./SearchBarAtoms";

export function QueryBadge({ item }: { item: PrimitiveAtom<QueryItem> }) {
const queryItem = useAtomValue(item);
const querySplitItemAction = useSetAtom(splitQueryAtom);
return (
<Badge key={queryItem.value} variant="primary">
{queryItem.text}
<button
className="ring-offset-base-2 focus:ring-primary-9 ml-1 rounded-full outline-none focus:ring-2 focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === "Enter") {
querySplitItemAction({ type: "remove", atom: item });
}
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={() => querySplitItemAction({ type: "remove", atom: item })}
>
<X className="text-base-8 hover:text-base-11 h-3 w-3" />
</button>
</Badge>
);
}
146 changes: 56 additions & 90 deletions packages/react/src/components/header/searchbar/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import * as React from "react";
import { X } from "lucide-react";

import { Command as CommandPrimitive } from "cmdk";
import {
Command,
Expand All @@ -9,63 +6,29 @@ import {
CommandItem,
CommandSeparator,
} from "@/shadcn/ui/command";
import { Badge } from "@/shadcn/ui/badge";
import { cn } from "@/lib/utils";
import { queryAtom, splitQueryAtom, useAutocomplete } from "./SearchBarAtoms";
import { useAtom } from "jotai";
import { JSON_SCHEMA, QueryItem } from "./types";
import { QueryBadge } from "./QueryBadge";
import { useTranslation } from "react-i18next";
import { HTMLAttributes, useRef, useState, useCallback } from "react";

type Framework = Record<"value" | "label", string>;

const FRAMEWORKS = [
{
value: "next.js",
label: "Next.js",
},
{
value: "sveltekit",
label: "SvelteKit",
},
{
value: "nuxt.js",
label: "Nuxt.js",
},
{
value: "remix",
label: "Remix",
},
{
value: "astro",
label: "Astro",
},
{
value: "wordpress",
label: "WordPress",
},
{
value: "express.js",
label: "Express.js",
},
{
value: "nest.js",
label: "Nest.js",
},
] satisfies Framework[];

export function SearchBar({ className }: React.HTMLAttributes<HTMLDivElement>) {
const inputRef = React.useRef<HTMLInputElement>(null);
const [open, setOpen] = React.useState(false);
const [selected, setSelected] = React.useState<Framework[]>([FRAMEWORKS[4]]);
const [inputValue, setInputValue] = React.useState("");

const handleUnselect = React.useCallback((framework: Framework) => {
setSelected((prev) => prev.filter((s) => s.value !== framework.value));
}, []);
export function SearchBar({ className }: HTMLAttributes<HTMLDivElement>) {
const inputRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [query, setQuery] = useAtom(queryAtom);
const [queryPieces, setQueryPieces] = useAtom(splitQueryAtom);
const { search, updateSearch, queryState, autocomplete } = useAutocomplete();

const handleKeyDown = React.useCallback(
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
const input = inputRef.current;
if (input) {
if (e.key === "Delete" || e.key === "Backspace") {
if (input.value === "") {
setSelected((prev) => {
setQuery((prev) => {
const newSelected = [...prev];
newSelected.pop();
return newSelected;
Expand All @@ -78,77 +41,80 @@ export function SearchBar({ className }: React.HTMLAttributes<HTMLDivElement>) {
}
}
},
[],
);

const selectables = FRAMEWORKS.filter(
(framework) => !selected.includes(framework),
[setQuery],
);

return (
<Command
onKeyDown={handleKeyDown}
className={cn("overflow-visible bg-transparent", className)}
>
<div className="group rounded-md border border-base px-3 py-2 text-sm ring-offset-base-2 focus-within:ring-2 focus-within:ring-primary focus-within:ring-offset-2">
<div className="border-base ring-offset-base-2 focus-within:ring-primary group rounded-md border px-3 py-2 text-sm focus-within:ring-2 focus-within:ring-offset-2">
<div className="flex flex-wrap gap-1">
{selected.map((framework) => {
return (
<Badge key={framework.value} variant="primary">
{framework.label}
<button
className="ml-1 rounded-full outline-none ring-offset-base-2 focus:ring-2 focus:ring-primary-9 focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === "Enter") {
handleUnselect(framework);
}
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={() => handleUnselect(framework)}
>
<X className="h-3 w-3 text-base-8 hover:text-base-11" />
</button>
</Badge>
);
{queryPieces.map((queryItem, i) => {
return <QueryBadge item={queryItem} key={"badge" + i} />;
})}
{/* Avoid having the "Search" Icon */}
<CommandPrimitive.Input
ref={inputRef}
value={inputValue}
onValueChange={setInputValue}
value={search}
onValueChange={updateSearch}
onBlur={() => setOpen(false)}
onFocus={() => setOpen(true)}
placeholder="Select frameworks..."
className="ml-2 flex-1 bg-transparent outline-none placeholder:text-base-8"
className="placeholder:text-base-8 ml-2 flex-1 bg-transparent outline-none"
/>
</div>
</div>
<div className="relative mt-2">
{open && selectables.length > 0 ? (
{open && autocomplete.length > 0 ? (
<>
<div className="absolute top-0 z-10 w-full rounded-md border border-base bg-base-1 text-base-11 shadow-md outline-none animate-in">
<div className="border-base bg-base-1 text-base-11 animate-in absolute top-0 z-10 w-full rounded-md border shadow-md outline-none">
<CommandGroup heading="Search Options" />
<CommandSeparator />
<CommandGroup className="h-full overflow-auto">
<CommandEmpty>Nothing to search?</CommandEmpty>
{selectables.map((framework) => {
{autocomplete.map((item) => {
return (
<CommandItem
key={framework.value}
key={item.type + item.value}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
value={item.type + item.value}
onSelect={(_) => {
setInputValue("");
setSelected((prev) => [...prev, framework]);
if (item.incomplete) {
console.log("autocompleteItem - incomplete", item);
updateSearch(
t(`search.class.${item.type}`, item.type) + ":",
);
return;
}

if (
JSON_SCHEMA[item.type].validation === undefined ||
JSON_SCHEMA[item.type].validation?.(item, query)
) {
console.log("autocompleteItem - trigger", item);
if (item.replace) {
setQuery((q) =>
q
.filter((i) => i.type !== item.type)
.concat(item),
);
} else {
setQueryPieces({
type: "insert",
value: item,
});
}

updateSearch("");
} // onClick?.(e);
}}
className={"cursor-pointer"}
>
{framework.label}
{item.type} : {item.text}
</CommandItem>
);
})}
Expand Down
Loading

0 comments on commit bacdb36

Please sign in to comment.