mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Can edit weapons in SendouQ settings
This commit is contained in:
parent
765b3d8004
commit
1d7f736dee
|
|
@ -39,10 +39,11 @@ export function Button(props: ButtonProps) {
|
|||
minimal: variant === "minimal",
|
||||
"minimal-success": variant === "minimal-success",
|
||||
"minimal-destructive": variant === "minimal-destructive",
|
||||
loading: loading,
|
||||
"disabled-opaque": props.disabled,
|
||||
loading,
|
||||
tiny,
|
||||
})}
|
||||
disabled={loading}
|
||||
disabled={props.disabled || loading}
|
||||
{...rest}
|
||||
>
|
||||
{icon && React.cloneElement(icon, { className: "button-icon" })}
|
||||
|
|
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ export const MMR_TOPX_VISIBILITY_CUTOFF = 50;
|
|||
export const LFG_AMOUNT_OF_STAGES_TO_GENERATE = 7;
|
||||
|
||||
export const MINI_BIO_MAX_LENGTH = 280;
|
||||
export const LFG_WEAPON_POOL_MAX_LENGTH = 3;
|
||||
|
||||
export const CLOSE_MMR_LIMIT = 250;
|
||||
export const BIT_HIGHER_MMR_LIMIT = 750;
|
||||
|
|
|
|||
|
|
@ -24,9 +24,11 @@ export function findById(userId?: string) {
|
|||
export function update({
|
||||
userId,
|
||||
miniBio,
|
||||
weapons,
|
||||
}: {
|
||||
userId: string;
|
||||
miniBio: Nullable<string>;
|
||||
weapons: string[];
|
||||
}) {
|
||||
return db.user.update({ where: { id: userId }, data: { miniBio } });
|
||||
return db.user.update({ where: { id: userId }, data: { miniBio, weapons } });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,16 +10,23 @@ import {
|
|||
useTransition,
|
||||
} from "remix";
|
||||
import { Button } from "~/components/Button";
|
||||
import { MINI_BIO_MAX_LENGTH } from "~/constants";
|
||||
import {
|
||||
LFG_WEAPON_POOL_MAX_LENGTH,
|
||||
MINI_BIO_MAX_LENGTH,
|
||||
weapons,
|
||||
} from "~/constants";
|
||||
import styles from "~/styles/play-settings.css";
|
||||
import {
|
||||
falsyToNull,
|
||||
makeTitle,
|
||||
parseRequestFormData,
|
||||
requireUser,
|
||||
safeJSONParse,
|
||||
} from "~/utils";
|
||||
import * as User from "~/models/User.server";
|
||||
import { z } from "zod";
|
||||
import { Combobox } from "~/components/Combobox";
|
||||
import { WeaponImage } from "~/components/WeaponImage";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return {
|
||||
|
|
@ -36,6 +43,10 @@ const settingsActionSchema = z.object({
|
|||
falsyToNull,
|
||||
z.string().max(MINI_BIO_MAX_LENGTH).nullable()
|
||||
),
|
||||
weapons: z.preprocess(
|
||||
safeJSONParse,
|
||||
z.array(z.enum(weapons)).max(LFG_WEAPON_POOL_MAX_LENGTH)
|
||||
),
|
||||
});
|
||||
|
||||
export const action: ActionFunction = async ({ request, context }) => {
|
||||
|
|
@ -45,32 +56,46 @@ export const action: ActionFunction = async ({ request, context }) => {
|
|||
schema: settingsActionSchema,
|
||||
});
|
||||
|
||||
await User.update({ userId: user.id, miniBio: data.miniBio });
|
||||
await User.update({
|
||||
userId: user.id,
|
||||
miniBio: data.miniBio,
|
||||
weapons: data.weapons,
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export type SettingsLoaderData = {
|
||||
miniBio?: string;
|
||||
weapons: string[];
|
||||
};
|
||||
|
||||
export const loader: ActionFunction = async ({ context }) => {
|
||||
const user = requireUser(context);
|
||||
|
||||
const { miniBio } = (await User.findById(user.id)) ?? {};
|
||||
const { miniBio, weapons } = (await User.findById(user.id)) ?? {};
|
||||
|
||||
return json<SettingsLoaderData>({ miniBio: miniBio ?? undefined });
|
||||
return json<SettingsLoaderData>({
|
||||
miniBio: miniBio ?? undefined,
|
||||
weapons: weapons ?? [],
|
||||
});
|
||||
};
|
||||
|
||||
export default function PlaySettingsPage() {
|
||||
const data = useLoaderData<SettingsLoaderData>();
|
||||
const transition = useTransition();
|
||||
const [miniBio, setMiniBio] = React.useState(data.miniBio ?? "");
|
||||
const [weaponPool, setWeaponPool] = React.useState(data.weapons);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form method="post">
|
||||
<label className="play-settings__mini-bio-label" htmlFor="mini-bio">
|
||||
<input
|
||||
type="hidden"
|
||||
name="weapons"
|
||||
value={JSON.stringify(weaponPool)}
|
||||
/>
|
||||
<label className="play-settings__label" htmlFor="mini-bio">
|
||||
SendouQ Bio
|
||||
</label>
|
||||
<div className="play-settings__explanation">
|
||||
|
|
@ -92,10 +117,50 @@ export default function PlaySettingsPage() {
|
|||
>
|
||||
{miniBio.length}/{MINI_BIO_MAX_LENGTH}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<label className="play-settings__label mt-4" htmlFor="weapon-pool">
|
||||
Weapon pool
|
||||
</label>
|
||||
<div className="play-settings__explanation">
|
||||
What are your preferred weapons to play? Select up to{" "}
|
||||
{LFG_WEAPON_POOL_MAX_LENGTH}.
|
||||
</div>
|
||||
<Combobox
|
||||
options={weapons.filter((wpn) => !weaponPool.includes(wpn))}
|
||||
onChange={(val) => setWeaponPool((pool) => [...pool, val])}
|
||||
inputName="weapon-pool"
|
||||
placeholder="Luna Blaster"
|
||||
/>
|
||||
<ol className="play-settings__weapons">
|
||||
{weaponPool.map((wpn, i) => (
|
||||
<li key={wpn} className="play-settings__weapon-row">
|
||||
<WeaponImage className="play-settings__weapon" weapon={wpn} />{" "}
|
||||
{i + 1}) {wpn}{" "}
|
||||
<Button
|
||||
className="ml-auto"
|
||||
tiny
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setWeaponPool((pool) =>
|
||||
pool.filter((weaponInPool) => weaponInPool !== wpn)
|
||||
)
|
||||
}
|
||||
>
|
||||
✖
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
{weaponPool.length > LFG_WEAPON_POOL_MAX_LENGTH && (
|
||||
<div className="play-settings__error-text">
|
||||
You can have at most {LFG_WEAPON_POOL_MAX_LENGTH} weapons in your
|
||||
weapon pool
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
loading={transition.state === "submitting"}
|
||||
loadingText="Saving..."
|
||||
disabled={weaponPool.length > LFG_WEAPON_POOL_MAX_LENGTH}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ button > .button-icon {
|
|||
margin-inline-end: var(--s-1-5);
|
||||
}
|
||||
|
||||
textarea {
|
||||
textarea:not(.plain) {
|
||||
padding: var(--s-2) var(--s-3);
|
||||
border: 1px solid var(--border);
|
||||
accent-color: var(--theme-secondary);
|
||||
|
|
@ -202,6 +202,13 @@ textarea {
|
|||
white-space: pre;
|
||||
}
|
||||
|
||||
textarea:not(.plain):focus-within {
|
||||
border-color: transparent;
|
||||
|
||||
/* TODO: rectangle on Safari */
|
||||
outline: 2px solid var(--theme);
|
||||
}
|
||||
|
||||
input:not(.plain) {
|
||||
height: 1rem;
|
||||
padding: var(--s-4) var(--s-3);
|
||||
|
|
@ -228,6 +235,7 @@ input:not(.plain):focus-within {
|
|||
|
||||
input:not(.plain)::placeholder {
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: var(--semi-bold);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
|
@ -443,6 +451,31 @@ hr {
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.four-zero-one__container {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -537,10 +570,22 @@ hr {
|
|||
margin-block-start: var(--s-4);
|
||||
}
|
||||
|
||||
.mt-6 {
|
||||
margin-block-start: var(--s-6);
|
||||
}
|
||||
|
||||
.mb-0 {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-block-end: var(--s-2);
|
||||
}
|
||||
|
||||
.ml-auto {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-inline-start: var(--s-2);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,8 +113,8 @@
|
|||
|
||||
.layout__main {
|
||||
max-width: 48rem;
|
||||
padding-top: var(--s-8);
|
||||
margin: 0 auto;
|
||||
padding-block: var(--s-8);
|
||||
padding-inline: var(--s-2);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,48 @@
|
|||
.play-settings__mini-bio-label {
|
||||
.play-settings__label {
|
||||
display: block;
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.play-settings__weapons {
|
||||
display: flex;
|
||||
width: 17rem;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
gap: var(--s-2);
|
||||
margin-block-start: var(--s-3);
|
||||
}
|
||||
|
||||
.play-settings__weapon-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--s-1) var(--s-2);
|
||||
background-color: var(--theme);
|
||||
border-radius: var(--rounded);
|
||||
color: var(--button-text);
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: var(--bold);
|
||||
letter-spacing: var(--s-0-5);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.play-settings__weapon {
|
||||
width: 2rem;
|
||||
margin-inline-end: var(--s-2);
|
||||
}
|
||||
|
||||
.play-settings__explanation {
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-sm);
|
||||
margin-block-end: var(--s-2);
|
||||
}
|
||||
|
||||
.play-settings__error-text {
|
||||
color: var(--theme-error);
|
||||
font-size: var(--fonts-sm);
|
||||
margin-block-start: var(--s-2);
|
||||
}
|
||||
|
||||
.play-settings__mini-bio {
|
||||
width: min(18rem, 100%);
|
||||
height: 6rem;
|
||||
|
|
|
|||
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -23,6 +23,7 @@
|
|||
"cookie-session": "^2.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"express": "^4.17.3",
|
||||
"fuse.js": "^6.5.3",
|
||||
"just-clone": "^5.0.1",
|
||||
"just-shuffle": "^4.0.1",
|
||||
"morgan": "^1.10.0",
|
||||
|
|
@ -4454,6 +4455,14 @@
|
|||
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fuse.js": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.5.3.tgz",
|
||||
"integrity": "sha512-sA5etGE7yD/pOqivZRBvUBd/NaL2sjAu6QuSaFoe1H2BrJSkH/T/UXAJ8CdXdw7DvY3Hs8CXKYkDWX7RiP5KOg==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||
|
|
@ -13703,6 +13712,11 @@
|
|||
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
|
||||
"dev": true
|
||||
},
|
||||
"fuse.js": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.5.3.tgz",
|
||||
"integrity": "sha512-sA5etGE7yD/pOqivZRBvUBd/NaL2sjAu6QuSaFoe1H2BrJSkH/T/UXAJ8CdXdw7DvY3Hs8CXKYkDWX7RiP5KOg=="
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
"cookie-session": "^2.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"express": "^4.17.3",
|
||||
"fuse.js": "^6.5.3",
|
||||
"just-clone": "^5.0.1",
|
||||
"just-shuffle": "^4.0.1",
|
||||
"morgan": "^1.10.0",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user