mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-28 22:31:22 -05:00
Add Combobox
This commit is contained in:
58
app/components/Combobox.tsx
Normal file
58
app/components/Combobox.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Combobox as HeadlessCombobox } from "@headlessui/react";
|
||||
import * as React from "react";
|
||||
import Fuse from "fuse.js";
|
||||
import clsx from "clsx";
|
||||
|
||||
const MAX_RESULTS_SHOWN = 6;
|
||||
|
||||
export function Combobox({
|
||||
options,
|
||||
onChange,
|
||||
inputName,
|
||||
placeholder,
|
||||
}: {
|
||||
options: string[] | readonly string[];
|
||||
onChange: (value: string) => void;
|
||||
inputName: string;
|
||||
placeholder: string;
|
||||
}) {
|
||||
const [query, setQuery] = React.useState("");
|
||||
|
||||
const filteredOptions = (() => {
|
||||
if (!query) return [];
|
||||
|
||||
const fuse = new Fuse(options);
|
||||
return fuse
|
||||
.search(query)
|
||||
.slice(0, MAX_RESULTS_SHOWN)
|
||||
.map((res) => res.item);
|
||||
})();
|
||||
|
||||
return (
|
||||
<HeadlessCombobox value="" onChange={onChange}>
|
||||
<HeadlessCombobox.Input
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="combobox-input"
|
||||
name={inputName}
|
||||
/>
|
||||
<HeadlessCombobox.Options
|
||||
className={clsx("combobox-options", {
|
||||
invisible: filteredOptions.length === 0,
|
||||
})}
|
||||
>
|
||||
{filteredOptions.map((option) => (
|
||||
<HeadlessCombobox.Option
|
||||
key={option}
|
||||
value={option}
|
||||
as={React.Fragment}
|
||||
>
|
||||
{({ active }) => (
|
||||
<li className={clsx("combobox-item", { active })}>{option}</li>
|
||||
)}
|
||||
</HeadlessCombobox.Option>
|
||||
))}
|
||||
</HeadlessCombobox.Options>
|
||||
</HeadlessCombobox>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { ActionFunction } from "@remix-run/node";
|
||||
import { redirect } from "@remix-run/node";
|
||||
import { Form, useMatches, useNavigate, useParams } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { z } from "zod";
|
||||
import { Button } from "~/components/Button";
|
||||
import { Button, LinkButton } from "~/components/Button";
|
||||
import { Dialog } from "~/components/Dialog";
|
||||
import { Redirect } from "~/components/Redirect";
|
||||
import { PlUS_SUGGESTION_COMMENT_MAX_LENGTH } from "~/constants";
|
||||
@@ -96,13 +95,13 @@ export default function PlusCommentModalPage() {
|
||||
<Button type="submit" data-cy="submit-button">
|
||||
Submit
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => navigate(PLUS_SUGGESTIONS_PAGE)}
|
||||
<LinkButton
|
||||
to={PLUS_SUGGESTIONS_PAGE}
|
||||
variant="minimal-destructive"
|
||||
tiny
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</LinkButton>
|
||||
</div>
|
||||
</Form>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMatches, useNavigate } from "@remix-run/react";
|
||||
import { Button } from "~/components/Button";
|
||||
import { useMatches } from "@remix-run/react";
|
||||
import { Button, LinkButton } from "~/components/Button";
|
||||
import { Dialog } from "~/components/Dialog";
|
||||
import { Redirect } from "~/components/Redirect";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
@@ -10,9 +10,9 @@ import * as React from "react";
|
||||
import { Label } from "~/components/Label";
|
||||
import { PlUS_SUGGESTION_COMMENT_MAX_LENGTH, PLUS_TIERS } from "~/constants";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Combobox } from "~/components/Combobox";
|
||||
|
||||
export default function PlusNewSuggestionModalPage() {
|
||||
const navigate = useNavigate();
|
||||
const user = useUser();
|
||||
const matches = useMatches();
|
||||
const data = matches.at(-2)!.data as PlusSuggestionsLoaderData;
|
||||
@@ -32,8 +32,8 @@ export default function PlusNewSuggestionModalPage() {
|
||||
<div className="stack md">
|
||||
<h2 className="plus__modal-title">Adding a new suggestion</h2>
|
||||
<div>
|
||||
<label>Tier</label>
|
||||
<select className="plus__modal-select">
|
||||
<label htmlFor="tier">Tier</label>
|
||||
<select id="tier" name="tier" className="plus__modal-select">
|
||||
{PLUS_TIERS.filter((tier) => {
|
||||
invariant(user?.plusTier);
|
||||
return tier >= user.plusTier;
|
||||
@@ -42,18 +42,27 @@ export default function PlusNewSuggestionModalPage() {
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="user">User</label>
|
||||
<Combobox
|
||||
options={["Seppo", "Heppo"]}
|
||||
onChange={(val) => console.log(val)}
|
||||
inputName="user"
|
||||
placeholder="Sendou#0043"
|
||||
/>
|
||||
</div>
|
||||
<CommentTextarea />
|
||||
<div className="plus__modal-buttons">
|
||||
<Button type="submit" data-cy="submit-button">
|
||||
Submit
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => navigate(PLUS_SUGGESTIONS_PAGE)}
|
||||
<LinkButton
|
||||
to={PLUS_SUGGESTIONS_PAGE}
|
||||
variant="minimal-destructive"
|
||||
tiny
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -60,6 +60,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
.combobox-input {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.combobox-options {
|
||||
position: absolute;
|
||||
width: 12rem;
|
||||
margin-top: var(--s-2);
|
||||
background-color: var(--bg-darker);
|
||||
border-radius: var(--rounded);
|
||||
color: var(--text);
|
||||
font-size: var(--fonts-sm);
|
||||
padding-block: var(--s-3);
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.combobox-item {
|
||||
padding: var(--s-1) var(--s-3);
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.combobox-item.active {
|
||||
background-color: var(--theme-transparent);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background-color: var(--bg-lighter);
|
||||
background-image: url("/svg/background-pattern.svg");
|
||||
|
||||
@@ -303,7 +303,7 @@ select {
|
||||
|
||||
/* TODO: Get color from CSS var */
|
||||
background-image: url('data:image/svg+xml;utf8,<svg width="1rem" color="rgb(255 255 255 / 55%)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg>');
|
||||
background-position-x: 95%;
|
||||
background-position-x: 92%;
|
||||
background-position-y: 50%;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: var(--rounded);
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"better-sqlite3": "^7.5.3",
|
||||
"clsx": "^1.1.1",
|
||||
"countries-list": "^2.6.1",
|
||||
"fuse.js": "^6.6.2",
|
||||
"randomcolor": "^0.6.2",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
@@ -6714,6 +6715,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/fuse.js": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz",
|
||||
"integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/gauge": {
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||
@@ -19072,6 +19081,11 @@
|
||||
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
|
||||
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="
|
||||
},
|
||||
"fuse.js": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz",
|
||||
"integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA=="
|
||||
},
|
||||
"gauge": {
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"better-sqlite3": "^7.5.3",
|
||||
"clsx": "^1.1.1",
|
||||
"countries-list": "^2.6.1",
|
||||
"fuse.js": "^6.6.2",
|
||||
"randomcolor": "^0.6.2",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
|
||||
Reference in New Issue
Block a user