Add new buttons (#2368)

* Initial

* Buttons everywhere

* Better 401 text
This commit is contained in:
Kalle 2025-06-05 21:44:05 +03:00 committed by GitHub
parent 391ac6a1c4
commit b621e2ff96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 193 additions and 60 deletions

View File

@ -0,0 +1,48 @@
.addNewButton {
--picture-size: 18px;
--icon-size: 14px;
--button-height: 28px;
--border-width: 2px;
--inner-border-radius: 6px;
padding-block: 0 !important;
padding-inline: 0 !important;
background-color: transparent !important;
height: var(--button-height);
}
.iconsContainer {
display: flex;
gap: var(--s-0-5);
align-items: center;
justify-content: center;
background-color: var(--theme-very-transparent);
height: calc(var(--button-height) - var(--border-width) * 2);
border-radius: var(--inner-border-radius) 0 0 var(--inner-border-radius);
padding-inline: var(--s-1);
}
.iconsContainer > svg {
max-width: var(--icon-size);
max-height: var(--icon-size);
min-width: var(--icon-size);
min-height: var(--icon-size);
color: var(--theme);
stroke-width: 4px;
}
.iconsContainer > picture {
max-width: var(--picture-size);
max-height: var(--picture-size);
min-width: var(--picture-size);
min-height: var(--picture-size);
}
.textContainer {
padding-inline: var(--s-1-5);
background-color: var(--theme) !important;
height: calc(var(--button-height) - var(--border-width) * 2);
display: flex;
align-items: center;
border-radius: 0 var(--inner-border-radius) var(--inner-border-radius) 0;
}

View File

@ -0,0 +1,23 @@
import { LinkButton } from "~/components/Button";
import { Image } from "~/components/Image";
import { PlusIcon } from "~/components/icons/Plus";
import { navIconUrl } from "~/utils/urls";
import styles from "./AddNewButton.module.css";
interface AddNewButtonProps {
to: string;
navIcon: string;
}
export function AddNewButton({ to, navIcon }: AddNewButtonProps) {
return (
<LinkButton to={to} size="tiny" className={styles.addNewButton}>
<span className={styles.iconsContainer}>
<PlusIcon />
<Image path={navIconUrl(navIcon)} size={18} alt="" />
</span>
<span className={styles.textContainer}>New</span>
</LinkButton>
);
}

View File

@ -68,21 +68,23 @@ export function Catcher() {
switch (error.status) {
case 401:
if (!user) {
return (
<Main>
<h2>Authentication required</h2>
<p>This page requires you to be logged in.</p>
<form action={LOG_IN_URL} method="post" className="mt-2">
<SendouButton type="submit" variant="minimal">
Log in via Discord
</SendouButton>
</form>
</Main>
);
}
return (
<Main>
<h2>Error 401 Unauthorized</h2>
{user ? (
<GetHelp />
) : (
<form action={LOG_IN_URL} method="post">
<p className="button-text-paragraph">
You should try{" "}
<SendouButton type="submit" variant="minimal">
logging in
</SendouButton>
</p>
</form>
)}
<GetHelp />
</Main>
);
case 403:

View File

@ -2,6 +2,7 @@ import type { MetaFunction, SerializeFrom } from "@remix-run/node";
import type { ShouldRevalidateFunction } from "@remix-run/react";
import { useLoaderData, useSearchParams } from "@remix-run/react";
import { useTranslation } from "react-i18next";
import { AddNewButton } from "~/components/AddNewButton";
import { Combobox } from "~/components/Combobox";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
@ -9,7 +10,7 @@ import { SendouButton } from "~/components/elements/Button";
import { SendouSwitch } from "~/components/elements/Switch";
import { CrossIcon } from "~/components/icons/Cross";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { artPage, navIconUrl } from "~/utils/urls";
import { artPage, navIconUrl, newArtPage } from "~/utils/urls";
import { metaTags } from "../../../utils/remix";
import { FILTERED_TAG_KEY_SEARCH_PARAM_KEY } from "../art-constants";
import { ArtGrid } from "../components/ArtGrid";
@ -85,24 +86,27 @@ export default function ArtPage() {
{t("art:openCommissionsOnly")}
</Label>
</div>
<Combobox
key={filteredTag}
options={data.allTags.map((t) => ({
label: t.name,
value: String(t.id),
}))}
inputName="tags"
placeholder={t("art:filterByTag")}
initialValue={null}
onChange={(selection) => {
if (!selection) return;
<div className="stack horizontal sm items-center">
<Combobox
key={filteredTag}
options={data.allTags.map((t) => ({
label: t.name,
value: String(t.id),
}))}
inputName="tags"
placeholder={t("art:filterByTag")}
initialValue={null}
onChange={(selection) => {
if (!selection) return;
setSearchParams((prev) => {
prev.set(FILTERED_TAG_KEY_SEARCH_PARAM_KEY, selection.label);
return prev;
});
}}
/>
setSearchParams((prev) => {
prev.set(FILTERED_TAG_KEY_SEARCH_PARAM_KEY, selection.label);
return prev;
});
}}
/>
<AddNewButton navIcon="art" to={newArtPage()} />
</div>
</div>
{filteredTag ? (
<div className="text-xs text-lighter stack md horizontal items-center">

View File

@ -2,6 +2,7 @@ import { Link, Outlet, useFetcher, useLoaderData } from "@remix-run/react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useCopyToClipboard } from "react-use";
import { AddNewButton } from "~/components/AddNewButton";
import { Avatar } from "~/components/Avatar";
import { FormWithConfirm } from "~/components/FormWithConfirm";
import { Label } from "~/components/Label";
@ -14,7 +15,7 @@ import { TrashIcon } from "~/components/icons/Trash";
import { useUser } from "~/features/auth/core/user";
import { useHasPermission } from "~/modules/permissions/hooks";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { associationsPage, userPage } from "~/utils/urls";
import { associationsPage, newAssociationsPage, userPage } from "~/utils/urls";
import { action } from "~/features/associations/actions/associations.server";
import {
@ -33,7 +34,12 @@ export default function AssociationsPage() {
return (
<Main className="stack lg">
<Outlet />
<Header />
<div className="stack sm">
<div className="stack items-end">
<AddNewButton to={newAssociationsPage()} navIcon="associations" />
</div>
<Header />
</div>
<JoinForm />
{data.associations.map((association) => (
<Association key={association.id} association={association} />

View File

@ -1,8 +1,10 @@
import type { MetaFunction } from "@remix-run/node";
import { Link } from "@remix-run/react";
import { useTranslation } from "react-i18next";
import { AddNewButton } from "~/components/AddNewButton";
import { Image } from "~/components/Image";
import { Main } from "~/components/Main";
import { useUser } from "~/features/auth/core/user";
import type { MainWeaponId } from "~/modules/in-game-lists/types";
import {
weaponCategories,
@ -14,6 +16,7 @@ import {
mainWeaponImageUrl,
mySlugify,
navIconUrl,
userNewBuildPage,
weaponBuildPage,
weaponCategoryUrl,
} from "~/utils/urls";
@ -41,6 +44,7 @@ export const handle: SendouRouteHandle = {
};
export default function BuildsPage() {
const user = useUser();
const { t } = useTranslation(["common", "weapons"]);
const weaponIdToSlug = (weaponId: MainWeaponId) => {
@ -49,6 +53,11 @@ export default function BuildsPage() {
return (
<Main className="stack md">
{user ? (
<div className="stack items-end">
<AddNewButton navIcon="builds" to={userNewBuildPage(user)} />
</div>
) : null}
{weaponCategories.map((category) => (
<div key={category.name} className="builds__category">
<div className="builds__category__header">

View File

@ -4,6 +4,7 @@ import clsx from "clsx";
import type * as React from "react";
import type { DateValue } from "react-aria-components";
import { useTranslation } from "react-i18next";
import { AddNewButton } from "~/components/AddNewButton";
import { CopyToClipboardPopover } from "~/components/CopyToClipboardPopover";
import { Main } from "~/components/Main";
import {
@ -24,7 +25,9 @@ import { dayMonthYearToDateValue } from "~/utils/dates";
import { metaTags } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix.server";
import {
CALENDAR_NEW_PAGE,
CALENDAR_PAGE,
TOURNAMENT_NEW_PAGE,
calendarIcalFeed,
calendarPage,
navIconUrl,
@ -103,6 +106,8 @@ export default function CalendarPage() {
key={CalendarEvent.filtersToString(data.filters)}
filters={data.filters}
/>
<AddNewButton navIcon="calendar" to={CALENDAR_NEW_PAGE} />
<AddNewButton navIcon="medal" to={TOURNAMENT_NEW_PAGE} />
</div>
</div>
<div

View File

@ -3,6 +3,7 @@ import { useFetcher, useLoaderData } from "@remix-run/react";
import { add, sub } from "date-fns";
import React from "react";
import { useTranslation } from "react-i18next";
import { AddNewButton } from "~/components/AddNewButton";
import { Alert } from "~/components/Alert";
import { Main } from "~/components/Main";
import { SubmitButton } from "~/components/SubmitButton";
@ -12,7 +13,7 @@ import { databaseTimestampToDate } from "~/utils/dates";
import { metaTags } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix.server";
import type { Unpacked } from "~/utils/types";
import { LFG_PAGE, navIconUrl } from "~/utils/urls";
import { LFG_PAGE, lfgNewPostPage, navIconUrl } from "~/utils/urls";
import { LFGAddFilterButton } from "../components/LFGAddFilterButton";
import { LFGFilters } from "../components/LFGFilters";
import { LFGPost } from "../components/LFGPost";
@ -103,11 +104,12 @@ export default function LFGPage() {
return (
<Main className="stack xl">
<div className="stack horizontal justify-end">
<div className="stack sm horizontal justify-end">
<LFGAddFilterButton
addFilter={(newFilter) => setFilters([...filters, newFilter])}
filters={filters}
/>
<AddNewButton navIcon="lfg" to={lfgNewPostPage()} />
</div>
<LFGFilters
filters={filters}

View File

@ -1,8 +1,13 @@
import { Outlet } from "@remix-run/react";
import { AddNewButton } from "~/components/AddNewButton";
import { Main } from "~/components/Main";
import { SubNav, SubNavLink } from "~/components/SubNav";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { navIconUrl, plusSuggestionPage } from "~/utils/urls";
import {
navIconUrl,
plusSuggestionPage,
plusSuggestionsNewPage,
} from "~/utils/urls";
import "~/styles/plus.css";
@ -17,7 +22,10 @@ export const handle: SendouRouteHandle = {
export default function PlusPageLayout() {
return (
<Main>
<Main className="stack md">
<div className="stack items-end">
<AddNewButton navIcon="plus" to={plusSuggestionsNewPage()} />
</div>
<SubNav>
<SubNavLink to="suggestions">Suggestions</SubNavLink>
<SubNavLink to="voting/results">Results</SubNavLink>

View File

@ -5,6 +5,7 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import * as R from "remeda";
import type { z } from "zod";
import { AddNewButton } from "~/components/AddNewButton";
import { Avatar } from "~/components/Avatar";
import { LinkButton } from "~/components/Button";
import { Divider } from "~/components/Divider";
@ -26,6 +27,7 @@ import { metaTags } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix.server";
import {
associationsPage,
newScrimPostPage,
scrimPage,
userPage,
userSubmittedImage,
@ -88,16 +90,17 @@ export default function ScrimsPage() {
return (
<Main className="stack lg">
{user ? (
<div className="stack horizontal justify-between items-center">
<LinkButton
size="tiny"
to={associationsPage()}
className="mr-auto"
className={clsx("mr-auto", { invisible: !user })}
variant="outlined"
>
{t("scrims:associations.title")}
</LinkButton>
) : null}
<AddNewButton to={newScrimPostPage()} navIcon="scrims" />
</div>
{typeof scrimToRequestId === "number" ? (
<RequestScrimModal
postId={scrimToRequestId}

View File

@ -2,6 +2,7 @@ import type { MetaFunction } from "@remix-run/node";
import { Form, Link, useLoaderData, useSearchParams } from "@remix-run/react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { AddNewButton } from "~/components/AddNewButton";
import { Alert } from "~/components/Alert";
import { FormErrors } from "~/components/FormErrors";
import { Input } from "~/components/Input";
@ -17,6 +18,7 @@ import { joinListToNaturalString } from "~/utils/arrays";
import { metaTags } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix.server";
import {
NEW_TEAM_PAGE,
TEAM_SEARCH_PAGE,
navIconUrl,
teamPage,
@ -87,14 +89,17 @@ export default function TeamSearchPage() {
return (
<Main className="stack lg">
<NewTeamDialog />
<Input
className="team-search__input"
icon={<SearchIcon className="team-search__icon" />}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder={t("team:teamSearch.placeholder")}
testId="team-search-input"
/>
<div className="stack sm horizontal justify-between">
<Input
className="team-search__input"
icon={<SearchIcon className="team-search__icon" />}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder={t("team:teamSearch.placeholder")}
testId="team-search-input"
/>
<AddNewButton navIcon="t" to={NEW_TEAM_PAGE} />
</div>
<div className="mt-6 stack lg">
{itemsToDisplay.map((team, i) => (
<Link

View File

@ -1,7 +1,7 @@
.team-search__input {
height: 40px !important;
margin: 0 auto;
font-size: var(--fonts-lg);
max-width: 240px;
}
.team-search__icon {

View File

@ -1,11 +1,13 @@
import { useLoaderData, useMatches } from "@remix-run/react";
import { useTranslation } from "react-i18next";
import { AddNewButton } from "~/components/AddNewButton";
import { ART_SOURCES, type ArtSource } from "~/features/art/art-types";
import { ArtGrid } from "~/features/art/components/ArtGrid";
import { useUser } from "~/features/auth/core/user";
import { useSearchParamState } from "~/hooks/useSearchParamState";
import invariant from "~/utils/invariant";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { newArtPage } from "~/utils/urls";
import type { UserPageLoaderData } from "../loaders/u.$identifier.server";
import { action } from "../actions/u.$identifier.art.server";
@ -51,6 +53,9 @@ export default function UserArtPage() {
return (
<div className="stack md">
<div className="stack items-end">
<AddNewButton navIcon="art" to={newArtPage()} />
</div>
<div className="stack horizontal justify-between items-start text-xs text-lighter">
<div>
{data.unvalidatedArtCount > 0

View File

@ -1,6 +1,7 @@
import { useFetcher, useLoaderData, useMatches } from "@remix-run/react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { AddNewButton } from "~/components/AddNewButton";
import { BuildCard } from "~/components/BuildCard";
import { FormMessage } from "~/components/FormMessage";
import { Image, WeaponImage } from "~/components/Image";
@ -16,13 +17,13 @@ import { BUILD_SORT_IDENTIFIERS, type BuildSort } from "~/db/tables";
import { useUser } from "~/features/auth/core/user";
import { useSearchParamState } from "~/hooks/useSearchParamState";
import type { MainWeaponId } from "~/modules/in-game-lists/types";
import { mainWeaponIds } from "~/modules/in-game-lists/weapon-ids";
import { atOrError } from "~/utils/arrays";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { weaponCategoryUrl } from "~/utils/urls";
import { userNewBuildPage, weaponCategoryUrl } from "~/utils/urls";
import type { UserPageLoaderData } from "../loaders/u.$identifier.server";
import { DEFAULT_BUILD_SORT } from "../user-page-constants";
import { mainWeaponIds } from "~/modules/in-game-lists/weapon-ids";
import { action } from "../actions/u.$identifier.builds.server";
import {
type UserBuildsPageData,
@ -91,6 +92,7 @@ export default function UserBuildsPage() {
>
{t("user:builds.sorting.changeButton")}
</SendouButton>
<AddNewButton navIcon="builds" to={userNewBuildPage(user)} />
</div>
)}
<BuildsFilters

View File

@ -88,7 +88,7 @@ export default function UserPageLayout() {
{t("common:results")} ({allResultsCount})
</SubNavLink>
)}
{data.user.buildsCount > 0 && (
{(data.user.buildsCount > 0 || isOwnPage) && (
<SubNavLink
to={userBuildsPage(data.user)}
prefetch="intent"
@ -97,12 +97,12 @@ export default function UserPageLayout() {
{t("common:pages.builds")} ({data.user.buildsCount})
</SubNavLink>
)}
{data.user.vodsCount > 0 && (
{(data.user.vodsCount > 0 || isOwnPage) && (
<SubNavLink to={userVodsPage(data.user)}>
{t("common:pages.vods")} ({data.user.vodsCount})
</SubNavLink>
)}
{data.user.artCount > 0 && (
{(data.user.artCount > 0 || isOwnPage) && (
<SubNavLink to={userArtPage(data.user)} end={false}>
{t("common:pages.art")} ({data.user.artCount})
</SubNavLink>

View File

@ -1,7 +1,9 @@
import { useLoaderData, useMatches } from "@remix-run/react";
import { AddNewButton } from "~/components/AddNewButton";
import { VodListing } from "~/features/vods/components/VodListing";
import invariant from "~/utils/invariant";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { newVodPage } from "~/utils/urls";
import { loader } from "../loaders/u.$identifier.vods.server";
export { loader };
@ -18,10 +20,15 @@ export default function UserVodsPage() {
const data = useLoaderData<typeof loader>();
return (
<div className="vods__listing__list">
{data.vods.map((vod) => (
<VodListing key={vod.id} vod={vod} showUser={false} />
))}
<div className="stack md">
<div className="stack items-end">
<AddNewButton navIcon="vods" to={newVodPage()} />
</div>
<div className="vods__listing__list">
{data.vods.map((vod) => (
<VodListing key={vod.id} vod={vod} showUser={false} />
))}
</div>
</div>
);
}

View File

@ -1,6 +1,7 @@
import type { MetaFunction } from "@remix-run/node";
import { useLoaderData, useSearchParams } from "@remix-run/react";
import { useTranslation } from "react-i18next";
import { AddNewButton } from "~/components/AddNewButton";
import { WeaponCombobox } from "~/components/Combobox";
import { Label } from "~/components/Label";
import { Main } from "~/components/Main";
@ -10,7 +11,7 @@ import { stageIds } from "~/modules/in-game-lists/stage-ids";
import { mainWeaponIds } from "~/modules/in-game-lists/weapon-ids";
import { metaTags } from "~/utils/remix";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { VODS_PAGE, navIconUrl } from "~/utils/urls";
import { VODS_PAGE, navIconUrl, newVodPage } from "~/utils/urls";
import { VodListing } from "../components/VodListing";
import { VODS_PAGE_BATCH_SIZE, videoMatchTypes } from "../vods-constants";
@ -52,7 +53,10 @@ export default function VodsSearchPage() {
return (
<Main className="stack lg" bigger>
<Filters addToSearchParams={addToSearchParams} />
<div className="stack sm horizontal justify-between items-start">
<Filters addToSearchParams={addToSearchParams} />
<AddNewButton navIcon="vods" to={newVodPage()} />
</div>
{data.vods.length > 0 ? (
<>
<div className="vods__listing__list">

View File

@ -50,7 +50,7 @@
"tag.desc.S2": "The game played is Splatoon 2.",
"tag.desc.SR": "Salmon Run event.",
"tag.desc.CARDS": "Tableturf Battle event.",
"icalFeed": "iCal feed",
"icalFeed": "iCal",
"filter.button": "Filter",
"filter.heading": "Filter calendar events",
"filter.modes": "Modes",