mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-11 21:56:11 -05:00
New footer (#3374)
This commit is contained in:
@@ -1,133 +1,235 @@
|
||||
.footer {
|
||||
container-type: inline-size;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--s-3);
|
||||
align-items: center;
|
||||
padding: var(--s-8) 0 var(--s-32);
|
||||
background-color: var(--color-bg-high);
|
||||
gap: var(--s-6);
|
||||
margin-block-start: auto;
|
||||
padding-block-end: var(--s-32);
|
||||
}
|
||||
|
||||
.linkList {
|
||||
.thanks {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
font-size: var(--font-2xs);
|
||||
|
||||
& > * {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.socials {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.socialIcon {
|
||||
height: 2.25rem;
|
||||
transition: transform 0.25s ease-in-out;
|
||||
}
|
||||
|
||||
.socialLink {
|
||||
display: flex;
|
||||
max-width: 10rem;
|
||||
height: 12rem;
|
||||
flex: 1 1 0;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--s-4);
|
||||
border-radius: var(--radius-box);
|
||||
background-color: var(--color-bg-higher);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-lg);
|
||||
|
||||
&:hover > .socialIcon {
|
||||
transform: translateY(-0.3rem);
|
||||
}
|
||||
}
|
||||
|
||||
.socialHeader {
|
||||
text-align: center;
|
||||
|
||||
& > p {
|
||||
font-size: var(--font-2xs);
|
||||
}
|
||||
}
|
||||
|
||||
.patronTitle {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: var(--weight-semi);
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.patronList {
|
||||
display: flex;
|
||||
max-width: 75vw;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
font-size: var(--font-xs);
|
||||
gap: var(--s-1);
|
||||
list-style: none;
|
||||
margin-block-start: var(--s-2);
|
||||
text-align: center;
|
||||
margin-block-start: var(--s-6);
|
||||
}
|
||||
|
||||
.patron {
|
||||
.thanksTitle {
|
||||
font-size: var(--font-md);
|
||||
font-weight: var(--weight-bold);
|
||||
}
|
||||
|
||||
.thanksSubtitle {
|
||||
font-size: var(--font-xs);
|
||||
color: var(--color-text-high);
|
||||
}
|
||||
|
||||
.marquee {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
gap: var(--s-2);
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
black 8%,
|
||||
black 92%,
|
||||
transparent
|
||||
);
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
padding-inline: var(--s-4);
|
||||
mask-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
.marqueeRow {
|
||||
display: flex;
|
||||
gap: var(--s-2);
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.marqueeGroup {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
gap: var(--s-2);
|
||||
animation: scroll 600s linear infinite;
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
animation: none;
|
||||
|
||||
&[aria-hidden="true"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.marqueeRow:nth-child(2) > & {
|
||||
animation-duration: 740s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
.marqueeRow:nth-child(3) > & {
|
||||
animation-duration: 670s;
|
||||
}
|
||||
|
||||
.marquee:hover & {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scroll {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(calc(-100% - var(--s-2)));
|
||||
}
|
||||
}
|
||||
|
||||
.chip {
|
||||
display: block;
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
font-size: var(--font-xs);
|
||||
padding: var(--s-1) var(--s-2-5);
|
||||
border-radius: var(--radius-full);
|
||||
background-color: var(--color-bg-higher);
|
||||
color: var(--color-text-accent);
|
||||
}
|
||||
|
||||
.copyrightNote {
|
||||
.cta {
|
||||
margin-block: calc(var(--s-2) * -1) var(--s-6);
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
max-width: 48rem;
|
||||
gap: var(--s-2);
|
||||
padding-inline: var(--s-4);
|
||||
|
||||
@container (max-width: 640px) {
|
||||
display: grid;
|
||||
align-self: stretch;
|
||||
max-width: none;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@container (max-width: 420px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 10.5rem;
|
||||
gap: var(--s-2);
|
||||
padding: var(--s-2) var(--s-4);
|
||||
border-radius: var(--radius-box);
|
||||
background-color: var(--color-bg-higher);
|
||||
font-size: var(--font-sm);
|
||||
font-weight: var(--weight-semi);
|
||||
|
||||
@container (max-width: 640px) {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cardIcon {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& > span {
|
||||
font-size: var(--font-2xs);
|
||||
font-weight: var(--weight-body);
|
||||
color: var(--color-text-high);
|
||||
}
|
||||
}
|
||||
|
||||
.privacy {
|
||||
font-size: var(--font-2xs);
|
||||
}
|
||||
|
||||
.legal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--color-text-high);
|
||||
font-size: var(--font-2xs);
|
||||
text-align: center;
|
||||
padding-inline: var(--s-4);
|
||||
}
|
||||
|
||||
.sourceLink {
|
||||
margin-inline: auto;
|
||||
.versionLink {
|
||||
font-size: var(--font-2xs);
|
||||
color: var(--color-text-high);
|
||||
font-style: italic;
|
||||
color: var(--color-text-high);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.socials {
|
||||
flex-direction: column;
|
||||
.sitemap {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: 44rem;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--s-6);
|
||||
padding-inline: var(--s-4);
|
||||
|
||||
@container (max-width: 640px) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.socialLink {
|
||||
max-width: initial;
|
||||
flex-direction: row;
|
||||
|
||||
&:hover > .socialIcon {
|
||||
transform: translateX(-0.3rem);
|
||||
}
|
||||
}
|
||||
|
||||
.socialHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
text-align: initial;
|
||||
|
||||
& > p {
|
||||
margin-block-start: var(--s-1);
|
||||
}
|
||||
}
|
||||
|
||||
.socialIcon {
|
||||
height: 1.75rem;
|
||||
@container (max-width: 420px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.sitemapColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.sitemapTitle {
|
||||
font-size: var(--font-2xs);
|
||||
font-weight: var(--weight-bold);
|
||||
color: var(--color-text-high);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.sitemapLink {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-2);
|
||||
font-size: var(--font-xs);
|
||||
}
|
||||
|
||||
.sitemapIcon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1,78 +1,241 @@
|
||||
import { Braces, CircleHelp, Hand, Heart, HeartHandshake } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import * as R from "remeda";
|
||||
import { Config } from "~/config";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { leaderboardsPage } from "~/features/leaderboards/leaderboards-urls";
|
||||
import { usePatrons } from "~/hooks/swr";
|
||||
import { useHasRole } from "~/modules/permissions/hooks";
|
||||
import { GIT_COMMIT } from "~/utils/git-commit";
|
||||
import {
|
||||
ANALYZER_URL,
|
||||
API_PAGE,
|
||||
ART_PAGE,
|
||||
ARTICLES_MAIN_PAGE,
|
||||
ASSOCIATIONS_PAGE,
|
||||
BUILDS_PAGE,
|
||||
CALENDAR_PAGE,
|
||||
COMP_ANALYZER_URL,
|
||||
CONTRIBUTIONS_PAGE,
|
||||
EVENTS_PAGE,
|
||||
FAQ_PAGE,
|
||||
LFG_PAGE,
|
||||
LINKS_PAGE,
|
||||
LUTI_PAGE,
|
||||
MAPS_URL,
|
||||
NINTENDO_COMMUNITY_TOURNAMENTS_GUIDELINES_URL,
|
||||
navIconUrl,
|
||||
OBJECT_DAMAGE_CALCULATOR_URL,
|
||||
PLANNER_URL,
|
||||
PLUS_SUGGESTIONS_PAGE,
|
||||
PLUS_VOTING_RESULTS_PAGE,
|
||||
SENDOU_INK_DISCORD_URL,
|
||||
SENDOU_INK_GITHUB_URL,
|
||||
SENDOU_LOVE_EMOJI_PATH,
|
||||
SENDOUQ_PAGE,
|
||||
SENDOUQ_RULES_PAGE,
|
||||
SETTINGS_PAGE,
|
||||
SUPPORT_PAGE,
|
||||
scrimsPage,
|
||||
TIER_LIST_MAKER_URL,
|
||||
TIERS_PAGE,
|
||||
TROPHIES_PAGE,
|
||||
userPage,
|
||||
VODS_PAGE,
|
||||
WELCOME_PAGE,
|
||||
weaponParamsPage,
|
||||
XSEARCH_PAGE,
|
||||
} from "~/utils/urls";
|
||||
|
||||
import { LinkButton } from "../elements/Button";
|
||||
import { Image } from "../Image";
|
||||
import { DiscordIcon } from "../icons/Discord";
|
||||
import { GitHubIcon } from "../icons/GitHub";
|
||||
import { PatreonIcon } from "../icons/Patreon";
|
||||
import styles from "./Footer.module.css";
|
||||
|
||||
export function Footer() {
|
||||
const { t } = useTranslation();
|
||||
const user = useUser();
|
||||
const MARQUEE_ROWS_COUNT = 3;
|
||||
|
||||
const showPrivacySettings =
|
||||
Config.fuseEnabled && !user?.roles.includes("MINOR_SUPPORT");
|
||||
const SITEMAP_COLUMNS = [
|
||||
{
|
||||
title: "nav.play",
|
||||
items: [
|
||||
{ name: "sendouq", url: SENDOUQ_PAGE },
|
||||
{ name: "sendouq", url: SENDOUQ_RULES_PAGE, label: "footer.link.qRules" },
|
||||
{ name: "sendouq", url: TIERS_PAGE, label: "footer.link.tiers" },
|
||||
{ name: "scrims", url: scrimsPage() },
|
||||
{ name: "lfg", url: LFG_PAGE },
|
||||
{ name: "calendar", url: CALENDAR_PAGE },
|
||||
{ name: "calendar", url: EVENTS_PAGE, label: "footer.link.events" },
|
||||
{
|
||||
name: "leaderboards",
|
||||
url: leaderboardsPage({ type: "USER" }),
|
||||
label: "footer.link.userLeaderboard",
|
||||
},
|
||||
{
|
||||
name: "leaderboards",
|
||||
url: leaderboardsPage({ type: "TEAM" }),
|
||||
label: "footer.link.teamLeaderboard",
|
||||
},
|
||||
{
|
||||
name: "leaderboards",
|
||||
url: leaderboardsPage({ type: "XP-ALL" }),
|
||||
label: "footer.link.xpLeaderboard",
|
||||
},
|
||||
...(Config.showLutiNavItem
|
||||
? [{ name: "luti", url: LUTI_PAGE } as const]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "nav.tools",
|
||||
items: [
|
||||
{ name: "analyzer", url: ANALYZER_URL },
|
||||
{
|
||||
name: "analyzer",
|
||||
url: weaponParamsPage("splattershot"),
|
||||
label: "footer.link.params",
|
||||
},
|
||||
{ name: "comp-analyzer", url: COMP_ANALYZER_URL },
|
||||
{ name: "object-damage-calculator", url: OBJECT_DAMAGE_CALCULATOR_URL },
|
||||
{ name: "plans", url: PLANNER_URL },
|
||||
{ name: "maps", url: MAPS_URL },
|
||||
{ name: "tier-list-maker", url: TIER_LIST_MAKER_URL },
|
||||
{ name: "xsearch", url: XSEARCH_PAGE },
|
||||
{ name: "settings", url: SETTINGS_PAGE },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "nav.community",
|
||||
items: [
|
||||
{ name: "builds", url: BUILDS_PAGE },
|
||||
{
|
||||
name: "associations",
|
||||
url: ASSOCIATIONS_PAGE,
|
||||
label: "footer.link.associations",
|
||||
},
|
||||
{ name: "art", url: ART_PAGE },
|
||||
{ name: "articles", url: ARTICLES_MAIN_PAGE },
|
||||
{ name: "vods", url: VODS_PAGE },
|
||||
{ name: "trophies", url: TROPHIES_PAGE },
|
||||
{ name: "links", url: LINKS_PAGE },
|
||||
{
|
||||
name: "plus",
|
||||
url: PLUS_SUGGESTIONS_PAGE,
|
||||
label: "footer.link.plusSuggestions",
|
||||
},
|
||||
{
|
||||
name: "plus",
|
||||
url: PLUS_VOTING_RESULTS_PAGE,
|
||||
label: "footer.link.plusVotingResults",
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function Footer() {
|
||||
const { t } = useTranslation(["common", "front"]);
|
||||
const isPatron = useHasRole("MINOR_SUPPORT");
|
||||
|
||||
const showPrivacySettings = Config.fuseEnabled && !isPatron;
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<div className={styles.linkList}>
|
||||
<Link to={CONTRIBUTIONS_PAGE}>{t("pages.contributors")}</Link>
|
||||
<Link to={FAQ_PAGE}>{t("pages.faq")}</Link>
|
||||
<Link to={WELCOME_PAGE}>{t("pages.welcome")}</Link>
|
||||
<Link to={API_PAGE}>{t("pages.api")}</Link>
|
||||
{showPrivacySettings ? <div data-fuse-privacy-tool /> : null}
|
||||
</div>
|
||||
<div className={styles.socials}>
|
||||
<a
|
||||
className={styles.socialLink}
|
||||
<div className={styles.cards}>
|
||||
<FooterCard
|
||||
icon={<GitHubIcon className={styles.cardIcon} />}
|
||||
title="GitHub"
|
||||
subtitle={t("common:footer.github.subtitle")}
|
||||
href={SENDOU_INK_GITHUB_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className={styles.socialHeader}>
|
||||
GitHub<p>{t("footer.github.subtitle")}</p>
|
||||
</div>
|
||||
<GitHubIcon className={styles.socialIcon} />
|
||||
</a>
|
||||
<a
|
||||
className={styles.socialLink}
|
||||
/>
|
||||
<FooterCard
|
||||
icon={<DiscordIcon className={styles.cardIcon} />}
|
||||
title="Discord"
|
||||
subtitle={t("common:footer.discord.subtitle")}
|
||||
href={SENDOU_INK_DISCORD_URL}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className={styles.socialHeader}>
|
||||
Discord<p>{t("footer.discord.subtitle")}</p>
|
||||
</div>{" "}
|
||||
<DiscordIcon className={styles.socialIcon} />
|
||||
</a>
|
||||
<Link className={styles.socialLink} to={SUPPORT_PAGE}>
|
||||
<div className={styles.socialHeader}>
|
||||
Patreon<p>{t("footer.patreon.subtitle")}</p>
|
||||
</div>{" "}
|
||||
<PatreonIcon className={styles.socialIcon} />
|
||||
</Link>
|
||||
/>
|
||||
<FooterCard
|
||||
icon={<PatreonIcon className={styles.cardIcon} />}
|
||||
title="Patreon"
|
||||
subtitle={t("common:footer.patreon.subtitle")}
|
||||
to={SUPPORT_PAGE}
|
||||
/>
|
||||
<FooterCard
|
||||
icon={<HeartHandshake className={styles.cardIcon} />}
|
||||
title={t("common:pages.contributors")}
|
||||
subtitle={t("common:footer.contributors.subtitle")}
|
||||
to={CONTRIBUTIONS_PAGE}
|
||||
/>
|
||||
<FooterCard
|
||||
icon={<CircleHelp className={styles.cardIcon} />}
|
||||
title={t("common:pages.faq")}
|
||||
subtitle={t("common:footer.faq.subtitle")}
|
||||
to={FAQ_PAGE}
|
||||
/>
|
||||
<FooterCard
|
||||
icon={<Hand className={styles.cardIcon} />}
|
||||
title={t("common:pages.welcome")}
|
||||
subtitle={t("common:footer.welcome.subtitle")}
|
||||
to={WELCOME_PAGE}
|
||||
/>
|
||||
<FooterCard
|
||||
icon={<Braces className={styles.cardIcon} />}
|
||||
title={t("common:pages.api")}
|
||||
subtitle={t("common:footer.api.subtitle")}
|
||||
to={API_PAGE}
|
||||
/>
|
||||
</div>
|
||||
<PatronsList />
|
||||
<div className={styles.copyrightNote}>
|
||||
<div className={styles.thanks}>
|
||||
<Image alt="" path={SENDOU_LOVE_EMOJI_PATH} width={40} height={40} />
|
||||
<h4 className={styles.thanksTitle}>{t("common:footer.thanks")}</h4>
|
||||
<p className={styles.thanksSubtitle}>
|
||||
{t("common:footer.thanks.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<PatronMarquee />
|
||||
{isPatron ? null : (
|
||||
<LinkButton
|
||||
to={SUPPORT_PAGE}
|
||||
size="small"
|
||||
variant="primary"
|
||||
icon={<Heart fill="currentColor" />}
|
||||
className={styles.cta}
|
||||
>
|
||||
{t("common:footer.support")}
|
||||
</LinkButton>
|
||||
)}
|
||||
<div className={styles.sitemap}>
|
||||
{SITEMAP_COLUMNS.map((column) => (
|
||||
<nav
|
||||
key={column.title}
|
||||
className={styles.sitemapColumn}
|
||||
aria-label={t(`front:${column.title}`)}
|
||||
>
|
||||
<h5 className={styles.sitemapTitle}>
|
||||
{t(`front:${column.title}`)}
|
||||
</h5>
|
||||
{column.items.map((item) => (
|
||||
<Link key={item.url} to={item.url} className={styles.sitemapLink}>
|
||||
<Image
|
||||
path={navIconUrl(item.name)}
|
||||
alt=""
|
||||
size={18}
|
||||
containerClassName={styles.sitemapIcon}
|
||||
/>
|
||||
{"label" in item
|
||||
? t(`common:${item.label}`)
|
||||
: t(`common:pages.${item.name}`)}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
))}
|
||||
</div>
|
||||
{showPrivacySettings ? (
|
||||
<div className={styles.privacy} data-fuse-privacy-tool />
|
||||
) : null}
|
||||
<div className={styles.legal}>
|
||||
<p>
|
||||
sendou.ink © Copyright of Sendou and contributors 2019-{currentYear}.
|
||||
Original content & source code is licensed under the AGPL-3.0 license.
|
||||
@@ -97,37 +260,111 @@ export function Footer() {
|
||||
</div>
|
||||
{GIT_COMMIT ? (
|
||||
<a
|
||||
className={styles.sourceLink}
|
||||
className={styles.versionLink}
|
||||
href={`${SENDOU_INK_GITHUB_URL}/commits/${GIT_COMMIT}/`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t("footer.version")} {GIT_COMMIT.slice(0, 10)}
|
||||
{t("common:footer.version")} {GIT_COMMIT.slice(0, 10)}
|
||||
</a>
|
||||
) : null}
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
function PatronsList() {
|
||||
const { t } = useTranslation();
|
||||
type Patron = NonNullable<ReturnType<typeof usePatrons>["patrons"]>[number];
|
||||
|
||||
function PatronMarquee() {
|
||||
const { patrons } = usePatrons();
|
||||
|
||||
if (!patrons || patrons.length === 0) return null;
|
||||
|
||||
const rows = R.chunk(patrons, Math.ceil(patrons.length / MARQUEE_ROWS_COUNT));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4 className={styles.patronTitle}>
|
||||
{t("footer.thanks")}
|
||||
<Image alt="" path={SENDOU_LOVE_EMOJI_PATH} width={24} height={24} />
|
||||
</h4>
|
||||
<ul className={styles.patronList}>
|
||||
{patrons?.map((patron) => (
|
||||
<li key={patron.id}>
|
||||
<Link to={userPage(patron)} className={styles.patron}>
|
||||
{patron.username}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className={styles.marquee}>
|
||||
{rows.map((row) => (
|
||||
<div key={row[0].id} className={styles.marqueeRow}>
|
||||
<PatronChips patrons={row} />
|
||||
<PatronChips patrons={row} ariaHidden />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PatronChips({
|
||||
patrons,
|
||||
ariaHidden = false,
|
||||
}: {
|
||||
patrons: Array<Patron>;
|
||||
ariaHidden?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ul className={styles.marqueeGroup} aria-hidden={ariaHidden}>
|
||||
{patrons.map((patron) => (
|
||||
<li key={patron.id}>
|
||||
<Link
|
||||
to={userPage(patron)}
|
||||
className={styles.chip}
|
||||
data-custom-theme={patron.customTheme ? true : undefined}
|
||||
style={customThemeChipStyle(patron.customTheme)}
|
||||
tabIndex={ariaHidden ? -1 : undefined}
|
||||
>
|
||||
{patron.username}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterCard({
|
||||
icon,
|
||||
title,
|
||||
subtitle,
|
||||
href,
|
||||
to,
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
href?: string;
|
||||
to?: string;
|
||||
}) {
|
||||
const content = (
|
||||
<>
|
||||
{icon}
|
||||
<span className={styles.cardHeader}>
|
||||
{title}
|
||||
<span>{subtitle}</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<a className={styles.card} href={href} target="_blank" rel="noreferrer">
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link className={styles.card} to={to ?? "/"}>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function customThemeChipStyle(
|
||||
customTheme: Patron["customTheme"],
|
||||
): React.CSSProperties {
|
||||
if (!customTheme) return {};
|
||||
|
||||
return R.pickBy(
|
||||
customTheme,
|
||||
(value, key) =>
|
||||
value !== null && !key.includes("--_size") && !key.includes("--_border"),
|
||||
) as React.CSSProperties;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { LEADERBOARDS_PAGE } from "~/utils/urls";
|
||||
import type { LEADERBOARD_TYPES } from "./leaderboards-constants";
|
||||
import { leaderboardsSearchParams } from "./leaderboards-search-params";
|
||||
|
||||
export const leaderboardsPage = (args: {
|
||||
season?: number;
|
||||
type?: "USER" | "TEAM";
|
||||
type?: (typeof LEADERBOARD_TYPES)[number];
|
||||
}) =>
|
||||
leaderboardsSearchParams.href(LEADERBOARDS_PAGE, {
|
||||
season: args.season ?? null,
|
||||
|
||||
@@ -499,7 +499,15 @@ export function findModInfoById(id: number) {
|
||||
export function findAllPatrons() {
|
||||
return db
|
||||
.selectFrom("User")
|
||||
.select(["User.id", "User.discordId", "User.username", "User.patronTier"])
|
||||
.select([
|
||||
"User.id",
|
||||
"User.discordId",
|
||||
"User.username",
|
||||
"User.patronTier",
|
||||
asJson(
|
||||
sql<CustomTheme | null>`IIF(COALESCE("User"."patronTier", 0) >= 2, "User"."customTheme", null)`,
|
||||
).as("customTheme"),
|
||||
])
|
||||
.where("User.patronTier", "is not", null)
|
||||
.orderBy("User.patronTier", "desc")
|
||||
.orderBy("User.patronStartedAt", "asc")
|
||||
|
||||
@@ -125,6 +125,9 @@ export const COMP_ANALYZER_URL = "/comp-analyzer";
|
||||
export const OBJECT_DAMAGE_CALCULATOR_URL = "/object-damage-calculator";
|
||||
export const SCANNER_PAGE = "/scanner";
|
||||
export const VODS_PAGE = "/vods";
|
||||
export const ART_PAGE = "/art";
|
||||
export const XSEARCH_PAGE = "/xsearch";
|
||||
export const ASSOCIATIONS_PAGE = "/associations";
|
||||
export const LEADERBOARDS_PAGE = "/leaderboards";
|
||||
export const LINKS_PAGE = "/links";
|
||||
export const SENDOUQ_PAGE = "/q";
|
||||
@@ -144,6 +147,8 @@ export const FRIENDS_PAGE = "/friends";
|
||||
export const SETTINGS_PAGE = "/settings";
|
||||
export const LUTI_PAGE = "/luti";
|
||||
export const PLUS_VOTING_PAGE = "/plus/voting";
|
||||
export const PLUS_VOTING_RESULTS_PAGE = "/plus/voting/results";
|
||||
export const PLUS_SUGGESTIONS_PAGE = "/plus/suggestions";
|
||||
|
||||
const STATIC_ASSETS_URL = Config.staticAssetsUrl;
|
||||
|
||||
|
||||
4
changelog/2026-08-29-footer-redesign.md
Normal file
4
changelog/2026-08-29-footer-redesign.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
type: feature
|
||||
---
|
||||
The footer got a redesign
|
||||
@@ -28,7 +28,9 @@ export class ScrimsPage {
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.locators = {
|
||||
associationsLink: page.getByRole("link", { name: "Associations" }),
|
||||
associationsLink: page
|
||||
.getByRole("main")
|
||||
.getByRole("link", { name: "Associations" }),
|
||||
requestButtons: page.getByTestId("request-scrim-button"),
|
||||
viewRequestButtons: page.getByTestId("view-request-button"),
|
||||
acceptRequestButtons: page.getByTestId("confirm-modal-trigger-button"),
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "Støt hjemmesiden",
|
||||
"footer.thanks": "Tak til vores patrons for støtten",
|
||||
"footer.version": "",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "",
|
||||
"actions.save": "Gem",
|
||||
"actions.unsave": "",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "Support",
|
||||
"footer.thanks": "Danke an die Patrons für den Support",
|
||||
"footer.version": "",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "",
|
||||
"actions.save": "Speichern",
|
||||
"actions.unsave": "",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "Support",
|
||||
"footer.thanks": "Thanks to the patrons for the support",
|
||||
"footer.version": "Version",
|
||||
"footer.contributors.subtitle": "The people behind the site",
|
||||
"footer.faq.subtitle": "Common questions",
|
||||
"footer.welcome.subtitle": "New here? Get started",
|
||||
"footer.api.subtitle": "Programmatic access",
|
||||
"footer.thanks.subtitle": "sendou.ink is made possible by the following people",
|
||||
"footer.support": "Support sendou.ink",
|
||||
"footer.link.qRules": "SendouQ rules",
|
||||
"footer.link.tiers": "Ranking tiers",
|
||||
"footer.link.events": "Events",
|
||||
"footer.link.userLeaderboard": "SQ player rankings",
|
||||
"footer.link.teamLeaderboard": "SQ team rankings",
|
||||
"footer.link.xpLeaderboard": "X Battle rankings",
|
||||
"footer.link.params": "Weapon parameters",
|
||||
"footer.link.associations": "Associations",
|
||||
"footer.link.plusSuggestions": "Plus suggestions",
|
||||
"footer.link.plusVotingResults": "Plus voting results",
|
||||
"actions.share": "Share",
|
||||
"actions.save": "Save",
|
||||
"actions.unsave": "Unsave",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "Apoyar",
|
||||
"footer.thanks": "Gracias a los mecenas por su apoyo",
|
||||
"footer.version": "Versión",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "Compartir",
|
||||
"actions.save": "Guardar",
|
||||
"actions.unsave": "No guardar",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "Apoyar",
|
||||
"footer.thanks": "Gracias a los Patrons por su apoyo",
|
||||
"footer.version": "Versión",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "Compartir",
|
||||
"actions.save": "Guardar",
|
||||
"actions.unsave": "No guardar",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "Soutien",
|
||||
"footer.thanks": "Merci à nos Supporters pour le soutien",
|
||||
"footer.version": "",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "",
|
||||
"actions.save": "Sauvegarder",
|
||||
"actions.unsave": "",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "Soutien",
|
||||
"footer.thanks": "Merci à nos Supporters pour le soutien",
|
||||
"footer.version": "",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "",
|
||||
"actions.save": "Sauvegarder",
|
||||
"actions.unsave": "",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "תמיכה",
|
||||
"footer.thanks": "תודה לפטרונים על התמיכה",
|
||||
"footer.version": "",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "",
|
||||
"actions.save": "שמירה",
|
||||
"actions.unsave": "",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "Supporto",
|
||||
"footer.thanks": "Mille grazie ai supporter su Patreon!",
|
||||
"footer.version": "",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "",
|
||||
"actions.save": "Salva",
|
||||
"actions.unsave": "",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "寄付",
|
||||
"footer.thanks": "寄付のご協力ありがとうございます",
|
||||
"footer.version": "バージョン",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "共有",
|
||||
"actions.save": "保存",
|
||||
"actions.unsave": "保存解除",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "후원",
|
||||
"footer.thanks": "Patreon 후원자들에게 갑사드립니다",
|
||||
"footer.version": "",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "",
|
||||
"actions.save": "저장하기",
|
||||
"actions.unsave": "",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "Steun sendou.ink",
|
||||
"footer.thanks": "Met dank aan alle patrons voor de steun",
|
||||
"footer.version": "",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "",
|
||||
"actions.save": "Opslaan",
|
||||
"actions.unsave": "",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "Wsparcie",
|
||||
"footer.thanks": "Dziękujemy patronom za wsparcie",
|
||||
"footer.version": "",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "",
|
||||
"actions.save": "Zapisz",
|
||||
"actions.unsave": "",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "Suporte",
|
||||
"footer.thanks": "Obrigado aos patronos e patronesses pelo suporte",
|
||||
"footer.version": "",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "",
|
||||
"actions.save": "Salvar",
|
||||
"actions.unsave": "",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "Поддержать",
|
||||
"footer.thanks": "Спасибо меценатам за поддержку",
|
||||
"footer.version": "",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "",
|
||||
"actions.save": "Сохранить",
|
||||
"actions.unsave": "",
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"footer.patreon.subtitle": "赞助",
|
||||
"footer.thanks": "感谢 Patreon 上的赞助者",
|
||||
"footer.version": "版本",
|
||||
"footer.contributors.subtitle": "",
|
||||
"footer.faq.subtitle": "",
|
||||
"footer.welcome.subtitle": "",
|
||||
"footer.api.subtitle": "",
|
||||
"footer.thanks.subtitle": "",
|
||||
"footer.support": "",
|
||||
"footer.link.qRules": "",
|
||||
"footer.link.tiers": "",
|
||||
"footer.link.events": "",
|
||||
"footer.link.userLeaderboard": "",
|
||||
"footer.link.teamLeaderboard": "",
|
||||
"footer.link.xpLeaderboard": "",
|
||||
"footer.link.params": "",
|
||||
"footer.link.associations": "",
|
||||
"footer.link.plusSuggestions": "",
|
||||
"footer.link.plusVotingResults": "",
|
||||
"actions.share": "分享",
|
||||
"actions.save": "保存",
|
||||
"actions.unsave": "不保存",
|
||||
|
||||
Reference in New Issue
Block a user