User search page

Closes #1032
This commit is contained in:
Kalle
2022-12-05 21:58:12 +02:00
parent 847c573da5
commit 06c43e8f0a
13 changed files with 252 additions and 11 deletions

View File

@@ -7,19 +7,25 @@ export function Input({
maxLength,
defaultValue,
leftAddon,
icon,
pattern,
list,
"data-cy": dataCy,
value,
onChange,
}: {
name: string;
name?: string;
className?: string;
minLength?: number;
maxLength?: number;
defaultValue?: string;
leftAddon?: string;
icon?: React.ReactNode;
pattern?: string;
list?: string;
"data-cy"?: string;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}) {
return (
<div className={clsx("input-container", className)}>
@@ -32,7 +38,10 @@ export function Input({
pattern={pattern}
list={list}
data-cy={dataCy}
value={value}
onChange={onChange}
/>
{icon}
</div>
);
}

View File

@@ -45,12 +45,12 @@
]
},
{
"name": "articles",
"url": "a",
"name": "u",
"url": "u",
"prefetch": true,
"filters": [
"invert(30%) sepia(73%) saturate(4089%) hue-rotate(326deg) brightness(96%) contrast(104%)",
"invert(55%) sepia(20%) saturate(738%) hue-rotate(224deg) brightness(86%) contrast(88%)"
"invert(20%) sepia(99%) saturate(5628%) hue-rotate(0deg) brightness(95%) contrast(118%)",
"invert(53%) sepia(0%) saturate(1071%) hue-rotate(253deg) brightness(95%) contrast(84%)"
]
},
{
@@ -79,5 +79,14 @@
"invert(16%) sepia(80%) saturate(5634%) hue-rotate(272deg) brightness(91%) contrast(105%)",
"invert(71%) sepia(42%) saturate(4219%) hue-rotate(150deg) brightness(99%) contrast(86%)"
]
},
{
"name": "articles",
"url": "a",
"prefetch": true,
"filters": [
"invert(30%) sepia(73%) saturate(4089%) hue-rotate(326deg) brightness(96%) contrast(104%)",
"invert(55%) sepia(20%) saturate(738%) hue-rotate(224deg) brightness(86%) contrast(88%)"
]
}
]

View File

@@ -18,6 +18,7 @@ import findAllPlusMembersSql from "./findAllPlusMembers.sql";
import findAllPatronsSql from "./findAllPatrons.sql";
import addResultHighlightSql from "./addResultHighlight.sql";
import deleteAllResultHighlightsSql from "./deleteAllResultHighlights.sql";
import searchSql from "./search.sql";
const upsertStm = sql.prepare(upsertSql);
export function upsert(
@@ -144,3 +145,24 @@ export const updateResultHighlights = sql.transaction(
}
}
);
const searchStm = sql.prepare(searchSql);
export function search(input: string) {
const searchString = `%${input}%`;
return searchStm.all({
discordName: searchString,
inGameName: searchString,
twitter: searchString,
}) as Array<
Pick<
User,
| "discordId"
| "discordAvatar"
| "discordName"
| "discordDiscriminator"
| "customUrl"
| "inGameName"
>
>;
}

View File

@@ -0,0 +1,21 @@
select
"discordName",
"discordId",
"discordAvatar",
"discordDiscriminator",
"customUrl",
"inGameName"
from
"User"
left join "PlusTier" on "PlusTier"."userId" = "User"."id"
where
"discordName" like @discordName
or "inGameName" like @inGameName
or "twitter" like @twitter
order by
case
when "PlusTier"."tier" is null then 4
else "PlusTier"."tier"
end asc
limit
25

View File

@@ -21,10 +21,12 @@ import styles from "~/styles/u.css";
import invariant from "tiny-invariant";
import {
isCustomUrl,
navIconUrl,
userBuildsPage,
userEditProfilePage,
userPage,
userResultsPage,
USER_SEARCH_PAGE,
} from "~/utils/urls";
import { Main } from "~/components/Main";
@@ -42,6 +44,22 @@ export const meta: MetaFunction = ({ data }: { data: UserPageLoaderData }) => {
export const handle: SendouRouteHandle = {
i18n: "user",
breadcrumb: ({ match }) => {
const data = match.data as UserPageLoaderData;
return [
{
imgPath: navIconUrl("u"),
href: USER_SEARCH_PAGE,
type: "IMAGE",
},
{
text: data.discordName,
href: userPage(data),
type: "TEXT",
},
];
},
};
export const userParamsSchema = z.object({ identifier: z.string() });

105
app/routes/u.tsx Normal file
View File

@@ -0,0 +1,105 @@
import type { LinksFunction, LoaderArgs } from "@remix-run/node";
import { Main } from "~/components/Main";
import type { SendouRouteHandle } from "~/utils/remix";
import { navIconUrl, userPage, USER_SEARCH_PAGE } from "~/utils/urls";
import styles from "~/styles/u.css";
import { Input } from "~/components/Input";
import { SearchIcon } from "~/components/icons/Search";
import { db } from "~/db";
import { Link, useLoaderData, useSearchParams } from "@remix-run/react";
import { useDebounce } from "react-use";
import * as React from "react";
import { Avatar } from "~/components/Avatar";
import { discordFullName } from "~/utils/strings";
import { useTranslation } from "~/hooks/useTranslation";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
};
export const handle: SendouRouteHandle = {
i18n: ["user"],
breadcrumb: () => ({
imgPath: navIconUrl("u"),
href: USER_SEARCH_PAGE,
type: "IMAGE",
}),
};
export const loader = ({ request }: LoaderArgs) => {
const url = new URL(request.url);
const input = url.searchParams.get("q");
if (!input) return null;
return { users: db.users.search(input), input };
};
export default function UserSearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const [inputValue, setInputValue] = React.useState(
searchParams.get("q") ?? ""
);
useDebounce(
() => {
if (!inputValue) return;
setSearchParams({ q: inputValue });
},
1500,
[inputValue]
);
return (
<Main className="u-search__container">
<Input
className="u-search__input"
icon={<SearchIcon className="u-search__icon" />}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<UsersList />
</Main>
);
}
function UsersList() {
const { t } = useTranslation(["user"]);
const data = useLoaderData<typeof loader>();
if (!data) {
return <div className="u-search__info">{t("user:search.info")}</div>;
}
if (data.users.length === 0) {
return (
<div className="u-search__info">
{t("user:search.noResults", { query: data.input })}
</div>
);
}
return (
<ul className="u-search__users">
{data.users.map((user) => {
return (
<li key={user.discordId}>
<Link to={userPage(user)}>
<div className="u-search__user">
<Avatar size="sm" user={user} />
<div>
<div>{discordFullName(user)}</div>
{user.inGameName ? (
<div className="u-search__ign">
{t("user:ign.short")}: {user.inGameName}
</div>
) : null}
</div>
</div>
</Link>
</li>
);
})}
</ul>
);
}

View File

@@ -137,7 +137,7 @@ textarea:not(.plain) {
height: 8rem;
padding: var(--s-2-5) var(--s-3);
border: 2px solid var(--border);
border-radius: var(--rounded);
border-radius: var(--rounded-sm);
accent-color: var(--theme-secondary);
background-color: var(--bg-input);
color: var(--text);
@@ -166,7 +166,7 @@ input:not(.plain) {
height: 1rem;
padding: var(--s-4) var(--s-3);
border: 2px solid var(--border);
border-radius: var(--rounded);
border-radius: var(--rounded-sm);
accent-color: var(--theme-secondary);
background-color: var(--bg-input);
color: var(--text);
@@ -252,7 +252,7 @@ select {
width: 100%;
box-sizing: border-box;
border: 2px solid var(--border);
border-radius: var(--rounded);
border-radius: var(--rounded-sm);
background: var(--select-background, var(--bg-input));
/* TODO: Get color from CSS var */
@@ -415,7 +415,7 @@ dialog::backdrop {
.input-container {
display: flex;
border: 2px solid var(--border);
border-radius: var(--rounded);
border-radius: var(--rounded-sm);
accent-color: var(--theme-secondary);
background-color: var(--bg-input);
color: var(--text);
@@ -434,6 +434,7 @@ dialog::backdrop {
.input-container > input {
width: 100%;
border: none;
margin: auto 0;
}
/* stylelint-enable no-descending-specificity */
@@ -443,7 +444,7 @@ dialog::backdrop {
.input-addon {
display: grid;
border-radius: var(--rounded) 0 0 var(--rounded);
border-radius: var(--rounded-sm) 0 0 var(--rounded-sm);
background-color: var(--bg-lighter);
color: var(--text-lighter);
font-size: var(--fonts-xs);

View File

@@ -169,3 +169,54 @@
gap: var(--s-3);
list-style: none;
}
.u-search__container {
display: flex;
flex-direction: column;
gap: var(--s-12);
}
.u-search__users {
display: flex;
width: 325px;
flex-direction: column;
padding: 0;
margin: 0 auto;
gap: var(--s-6);
}
.u-search__user {
display: flex;
align-items: center;
font-weight: var(--body);
gap: var(--s-4);
line-height: 1.2rem;
}
.u-search__ign {
color: var(--text-lighter);
font-size: var(--fonts-sm);
font-weight: var(--body);
}
.u-search__users > li {
list-style: none;
}
.u-search__icon {
height: 25px;
margin: auto;
margin-right: 15px;
}
.u-search__info {
color: var(--text-lighter);
font-size: var(--fonts-sm);
text-align: center;
}
.u-search__input {
height: 40px !important;
margin: 0 auto;
font-size: var(--fonts-lg);
}

View File

@@ -44,6 +44,7 @@ export const FAQ_PAGE = "/faq";
export const CONTRIBUTIONS_PAGE = "/contributions";
export const BADGES_PAGE = "/badges";
export const BUILDS_PAGE = "/builds";
export const USER_SEARCH_PAGE = "/u";
export const CALENDAR_PAGE = "/calendar";
export const STOP_IMPERSONATING_URL = "/auth/impersonate/stop";
export const SEED_URL = "/seed";

View File

@@ -13,6 +13,7 @@
"pages.plans": "Planner",
"pages.object-damage-calculator": "DMG Calc",
"pages.myPage": "My Page",
"pages.u": "User Search",
"header.profile": "Profile",
"header.logout": "Log out",

View File

@@ -25,5 +25,8 @@
"forms.errors.invalidCustomUrl.numbers": "Custom URL can't only contain numbers",
"forms.errors.invalidCustomUrl.strangeCharacter": "Custom URL can't contain special characters",
"forms.errors.invalidCustomUrl.duplicate": "Someone is already using this custom URL",
"forms.errors.invalidSens": "Motion sens can't be set if R-stick sens isn't"
"forms.errors.invalidSens": "Motion sens can't be set if R-stick sens isn't",
"search.info": "Search for users by Discord, Splatoon 3 or Twitter name",
"search.noResults": "No users found matching '{{query}}'"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB