mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-25 07:32:19 -05:00
Better meta tags (#2083)
* Initial * Work * lint * Lint * Remove twitter * Progress * Progress * Progress * Progress * Progress * Progress * Fix
This commit is contained in:
parent
5196e6f845
commit
986355050d
|
|
@ -15,15 +15,18 @@ import { UserSearch } from "~/components/UserSearch";
|
|||
import { useUser } from "~/features/auth/core/user";
|
||||
import { FRIEND_CODE_REGEXP_PATTERN } from "~/features/sendouq/q-constants";
|
||||
import { isAdmin, isMod } from "~/permissions";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { SEED_URL, STOP_IMPERSONATING_URL, impersonateUrl } from "~/utils/urls";
|
||||
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import { action } from "../actions/admin.server";
|
||||
import { loader } from "../loaders/admin.server";
|
||||
export { action, loader };
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: makeTitle("Admin page") }];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Admin Panel",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export default function AdminPage() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import type { ActionFunction, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import type {
|
||||
ActionFunction,
|
||||
LoaderFunctionArgs,
|
||||
MetaFunction,
|
||||
} from "@remix-run/node";
|
||||
import {
|
||||
unstable_composeUploadHandlers as composeUploadHandlers,
|
||||
unstable_createMemoryUploadHandler as createMemoryUploadHandler,
|
||||
|
|
@ -38,6 +42,7 @@ import {
|
|||
navIconUrl,
|
||||
userArtPage,
|
||||
} from "~/utils/urls";
|
||||
import { metaTitle } from "../../../utils/remix";
|
||||
import { ART, NEW_ART_EXISTING_SEARCH_PARAM_KEY } from "../art-constants";
|
||||
import { editArtSchema, newArtSchema } from "../art-schemas.server";
|
||||
import { previewUrl } from "../art-utils";
|
||||
|
|
@ -54,6 +59,12 @@ export const handle: SendouRouteHandle = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return metaTitle({
|
||||
title: "New art",
|
||||
});
|
||||
};
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
const user = await requireUser(request);
|
||||
validate(user.isArtist, "Lacking artist role", 403);
|
||||
|
|
|
|||
|
|
@ -12,10 +12,9 @@ import { Label } from "~/components/Label";
|
|||
import { Main } from "~/components/Main";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { CrossIcon } from "~/components/icons/Cross";
|
||||
import i18next from "~/modules/i18n/i18next.server";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { artPage, navIconUrl } from "~/utils/urls";
|
||||
import { metaTags } from "../../../utils/remix";
|
||||
import { ArtGrid } from "../components/ArtGrid";
|
||||
import { allArtTags } from "../queries/allArtTags.server";
|
||||
import {
|
||||
|
|
@ -49,12 +48,16 @@ export const meta: MetaFunction = (args) => {
|
|||
|
||||
if (!data) return [];
|
||||
|
||||
return [{ title: data.title }];
|
||||
return metaTags({
|
||||
title: "Art",
|
||||
ogTitle: "Splatoon art showcase",
|
||||
description:
|
||||
"Splatoon art filterable by various tags. Find artist to commission for your own custom art. Includes various styles such as traditional, digital, 3D and SFM.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const t = await i18next.getFixedT(request);
|
||||
|
||||
const allTags = allArtTags();
|
||||
|
||||
const filteredTagName = new URL(request.url).searchParams.get(
|
||||
|
|
@ -65,7 +68,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
return {
|
||||
arts: filteredTag ? showcaseArtsByTag(filteredTag.id) : showcaseArts(),
|
||||
allTags,
|
||||
title: makeTitle(t("pages.art")),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ import { Main } from "~/components/Main";
|
|||
import invariant from "~/utils/invariant";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { notFoundIfFalsy } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
ARTICLES_MAIN_PAGE,
|
||||
articlePage,
|
||||
articlePreviewUrl,
|
||||
navIconUrl,
|
||||
} from "~/utils/urls";
|
||||
import { metaTags } from "../../../utils/remix";
|
||||
import { articleBySlug } from "../core/bySlug.server";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
@ -48,15 +48,14 @@ export const meta: MetaFunction = (args) => {
|
|||
|
||||
const description = data.content.trim().split("\n")[0];
|
||||
|
||||
return [
|
||||
{ title: makeTitle(data.title) },
|
||||
{ property: "og:title", content: data.title },
|
||||
{ name: "description", content: description },
|
||||
{ property: "og:description", content: description },
|
||||
{ property: "og:image", content: articlePreviewUrl(args.params.slug) },
|
||||
{ property: "og:type", content: "article" },
|
||||
{ property: "og:site_name", content: "sendou.ink" },
|
||||
];
|
||||
return metaTags({
|
||||
title: data.title,
|
||||
description,
|
||||
image: {
|
||||
url: articlePreviewUrl(args.params.slug),
|
||||
},
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const loader = ({ params }: LoaderFunctionArgs) => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { Link, useLoaderData } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Main } from "~/components/Main";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { ARTICLES_MAIN_PAGE, articlePage, navIconUrl } from "~/utils/urls";
|
||||
import { metaTags } from "../../../utils/remix";
|
||||
import { mostRecentArticles } from "../core/list.server";
|
||||
|
||||
import "~/styles/front.css";
|
||||
|
|
@ -18,6 +20,16 @@ export const handle: SendouRouteHandle = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Articles",
|
||||
ogTitle: "Splatoon articles",
|
||||
description:
|
||||
"Articles about the competitive side of Splatoon. Written by various community members.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const loader = async () => {
|
||||
return {
|
||||
articles: await mostRecentArticles(MAX_ARTICLES_COUNT),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { SerializeFrom } from "@remix-run/node";
|
||||
import type { MetaFunction, SerializeFrom } from "@remix-run/node";
|
||||
import { NavLink, Outlet, useLoaderData } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
|
@ -10,6 +10,7 @@ import { SearchIcon } from "~/components/icons/Search";
|
|||
import { useUser } from "~/features/auth/core/user";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { BADGES_DOC_LINK, BADGES_PAGE, navIconUrl } from "~/utils/urls";
|
||||
import { metaTags } from "../../../utils/remix";
|
||||
import * as BadgeRepository from "../BadgeRepository.server";
|
||||
|
||||
import "~/styles/badges.css";
|
||||
|
|
@ -23,6 +24,16 @@ export const handle: SendouRouteHandle = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Badges",
|
||||
ogTitle: "Splatoon badges (tournament prizes list)",
|
||||
location: args.location,
|
||||
description:
|
||||
"Over 400 badge tournament prizes and counting! Check out the full list including the owners.",
|
||||
});
|
||||
};
|
||||
|
||||
export type BadgesLoaderData = SerializeFrom<typeof loader>;
|
||||
|
||||
export const loader = async () => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { BeakerIcon } from "~/components/icons/Beaker";
|
|||
import { MAX_AP } from "~/constants";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useSetTitle } from "~/hooks/useSetTitle";
|
||||
import type { Ability as AbilityType } from "~/modules/in-game-lists";
|
||||
import {
|
||||
ANGLE_SHOOTER_ID,
|
||||
|
|
@ -37,7 +36,6 @@ import { atOrError, nullFilledArray, removeDuplicates } from "~/utils/arrays";
|
|||
import { damageTypeTranslationString } from "~/utils/i18next";
|
||||
import invariant from "~/utils/invariant";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
ANALYZER_URL,
|
||||
mainWeaponImageUrl,
|
||||
|
|
@ -49,6 +47,7 @@ import {
|
|||
} from "~/utils/urls";
|
||||
import { SendouButton } from "../../../components/elements/Button";
|
||||
import { SendouPopover } from "../../../components/elements/Popover";
|
||||
import { metaTags } from "../../../utils/remix";
|
||||
import {
|
||||
MAX_LDE_INTENSITY,
|
||||
damageTypeToWeaponType,
|
||||
|
|
@ -84,14 +83,14 @@ import { SendouSwitch } from "~/components/elements/Switch";
|
|||
|
||||
export const CURRENT_PATCH = "9.2";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: makeTitle("Build Analyzer") },
|
||||
{
|
||||
name: "description",
|
||||
content: "Detailed stats for any weapon and build in Splatoon 3.",
|
||||
},
|
||||
];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Build analyzer",
|
||||
ogTitle: "Splatoon 3 build analyzer/simulator",
|
||||
location: args.location,
|
||||
description:
|
||||
"Analyze and compare Splatoon 3 builds. Find out what exactly each combination of abilities does.",
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
@ -118,8 +117,7 @@ export default function BuildAnalyzerShell() {
|
|||
|
||||
function BuildAnalyzerPage() {
|
||||
const user = useUser();
|
||||
const { t } = useTranslation(["analyzer", "common", "weapons"]);
|
||||
useSetTitle(t("common:pages.analyzer"));
|
||||
const { t } = useTranslation(["analyzer", "weapons"]);
|
||||
const {
|
||||
build,
|
||||
build2,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import { cachified } from "@epic-web/cachified";
|
||||
import type {
|
||||
LoaderFunctionArgs,
|
||||
MetaFunction,
|
||||
SerializeFrom,
|
||||
} from "@remix-run/node";
|
||||
import type { LoaderFunctionArgs, SerializeFrom } from "@remix-run/node";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
|
@ -16,7 +12,6 @@ import {
|
|||
type SendouRouteHandle,
|
||||
notFoundIfNullLike,
|
||||
} from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { weaponNameSlugToId } from "~/utils/unslugify.server";
|
||||
import {
|
||||
BUILDS_PAGE,
|
||||
|
|
@ -27,13 +22,8 @@ import {
|
|||
import { popularBuilds } from "../build-stats-utils";
|
||||
import { abilitiesByWeaponId } from "../queries/abilitiesByWeaponId.server";
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader> | null;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [{ title: data.meta.title }];
|
||||
};
|
||||
import { meta } from "../../builds/routes/builds.$slug";
|
||||
export { meta };
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["analyzer", "builds"],
|
||||
|
|
@ -63,12 +53,10 @@ export const handle: SendouRouteHandle = {
|
|||
};
|
||||
|
||||
export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
const t = await i18next.getFixedT(request, ["builds", "weapons", "common"]);
|
||||
const t = await i18next.getFixedT(request, ["builds"]);
|
||||
const slug = params.slug;
|
||||
const weaponId = notFoundIfNullLike(weaponNameSlugToId(slug));
|
||||
|
||||
const weaponName = t(`weapons:MAIN_${weaponId}`);
|
||||
|
||||
const cachedPopularBuilds = await cachified({
|
||||
key: `popular-builds-${weaponId}`,
|
||||
cache,
|
||||
|
|
@ -83,11 +71,6 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
|||
meta: {
|
||||
weaponId,
|
||||
slug: slug!,
|
||||
title: makeTitle([
|
||||
t("builds:linkButton.popularBuilds"),
|
||||
weaponName,
|
||||
t("common:pages.builds"),
|
||||
]),
|
||||
breadcrumbText: t("builds:linkButton.popularBuilds"),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import { cachified } from "@epic-web/cachified";
|
||||
import type {
|
||||
LoaderFunctionArgs,
|
||||
MetaFunction,
|
||||
SerializeFrom,
|
||||
} from "@remix-run/node";
|
||||
import type { LoaderFunctionArgs, SerializeFrom } from "@remix-run/node";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Ability } from "~/components/Ability";
|
||||
|
|
@ -16,7 +12,6 @@ import {
|
|||
type SendouRouteHandle,
|
||||
notFoundIfNullLike,
|
||||
} from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { weaponNameSlugToId } from "~/utils/unslugify.server";
|
||||
import {
|
||||
BUILDS_PAGE,
|
||||
|
|
@ -29,13 +24,8 @@ import { averageAbilityPoints } from "../queries/averageAbilityPoints.server";
|
|||
|
||||
import "../build-stats.css";
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader> | null;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [{ title: data.meta.title }];
|
||||
};
|
||||
import { meta } from "../../builds/routes/builds.$slug";
|
||||
export { meta };
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["weapons", "builds", "analyzer"],
|
||||
|
|
@ -65,11 +55,9 @@ export const handle: SendouRouteHandle = {
|
|||
};
|
||||
|
||||
export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
const t = await i18next.getFixedT(request, ["builds", "weapons", "common"]);
|
||||
const t = await i18next.getFixedT(request, ["builds"]);
|
||||
const weaponId = notFoundIfNullLike(weaponNameSlugToId(params.slug));
|
||||
|
||||
const weaponName = t(`weapons:MAIN_${weaponId}`);
|
||||
|
||||
const cachedStats = await cachified({
|
||||
key: `build-stats-${weaponId}`,
|
||||
cache,
|
||||
|
|
@ -87,11 +75,6 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
|||
weaponId,
|
||||
meta: {
|
||||
slug: params.slug!,
|
||||
title: makeTitle([
|
||||
t("builds:linkButton.abilityStats"),
|
||||
weaponName,
|
||||
t("common:pages.builds"),
|
||||
]),
|
||||
breadcrumbText: t("builds:linkButton.abilityStats"),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import type { LoaderFunctionArgs } from "@remix-run/node";
|
|||
import { BUILDS_PAGE_BATCH_SIZE, BUILDS_PAGE_MAX_BUILDS } from "~/constants";
|
||||
import { i18next } from "~/modules/i18n/i18next.server";
|
||||
import { weaponIdIsNotAlt } from "~/modules/in-game-lists";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { weaponNameSlugToId } from "~/utils/unslugify.server";
|
||||
import { mySlugify } from "~/utils/urls";
|
||||
import { FILTER_SEARCH_PARAM_KEY } from "../builds-constants";
|
||||
|
|
@ -54,7 +53,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
|||
return {
|
||||
weaponId,
|
||||
weaponName,
|
||||
title: makeTitle([weaponName, t("common:pages.builds")]),
|
||||
builds: filteredBuilds,
|
||||
limit,
|
||||
slug,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import {
|
|||
PATCHES,
|
||||
} from "~/constants";
|
||||
import { safeJSONParse } from "~/utils/json";
|
||||
import { isRevalidation } from "~/utils/remix";
|
||||
import { isRevalidation, metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
import {
|
||||
|
|
@ -125,12 +125,15 @@ export const shouldRevalidate: ShouldRevalidateFunction = (args) => {
|
|||
return args.defaultShouldRevalidate;
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader> | null;
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [{ title: data.title }];
|
||||
return metaTags({
|
||||
title: `${args.data.weaponName} builds`,
|
||||
ogTitle: `${args.data.weaponName} Splatoon 3 builds`,
|
||||
description: `Collection of ${args.data.weaponName} builds from the top competitive players. Find the best combination of abilities and level up your gameplay.`,
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,9 @@ import { Link } from "@remix-run/react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Image } from "~/components/Image";
|
||||
import { Main } from "~/components/Main";
|
||||
import { useSetTitle } from "~/hooks/useSetTitle";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists";
|
||||
import { weaponCategories, weaponIdIsNotAlt } from "~/modules/in-game-lists";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
BUILDS_PAGE,
|
||||
mainWeaponImageUrl,
|
||||
|
|
@ -16,17 +14,18 @@ import {
|
|||
weaponBuildPage,
|
||||
weaponCategoryUrl,
|
||||
} from "~/utils/urls";
|
||||
import { metaTags } from "../../../utils/remix";
|
||||
|
||||
import "~/styles/builds.css";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: makeTitle("Builds") },
|
||||
{
|
||||
name: "description",
|
||||
content: "View Splatoon 3 builds for all weapons by the best players",
|
||||
},
|
||||
];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Builds",
|
||||
ogTitle: "Splatoon 3 builds for all weapons",
|
||||
description:
|
||||
"View Splatoon 3 builds for all weapons by the best players. Includes collection of user submitted builds and an aggregation of ability stats.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
@ -40,7 +39,6 @@ export const handle: SendouRouteHandle = {
|
|||
|
||||
export default function BuildsPage() {
|
||||
const { t } = useTranslation(["common", "weapons"]);
|
||||
useSetTitle(t("common:pages.builds"));
|
||||
|
||||
const weaponIdToSlug = (weaponId: MainWeaponId) => {
|
||||
return mySlugify(t(`weapons:MAIN_${weaponId}`, { lng: "en" }));
|
||||
|
|
|
|||
|
|
@ -5,15 +5,12 @@ import * as BadgeRepository from "~/features/badges/BadgeRepository.server";
|
|||
import * as CalendarRepository from "~/features/calendar/CalendarRepository.server";
|
||||
import { tournamentData } from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import * as TournamentOrganizationRepository from "~/features/tournament-organization/TournamentOrganizationRepository.server";
|
||||
import { i18next } from "~/modules/i18n/i18next.server";
|
||||
import { canEditCalendarEvent } from "~/permissions";
|
||||
import { validate } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { tournamentBracketsPage } from "~/utils/urls";
|
||||
import { canAddNewEvent } from "../calendar-utils";
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const t = await i18next.getFixedT(request);
|
||||
const user = await requireUser(request);
|
||||
const url = new URL(request.url);
|
||||
|
||||
|
|
@ -87,7 +84,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
user.isTournamentOrganizer && !eventToEdit
|
||||
? await CalendarRepository.findRecentTournamentsByAuthorId(user.id)
|
||||
: undefined,
|
||||
title: makeTitle([canEditEvent ? "Edit" : "New", t("pages.calendar")]),
|
||||
organizations: await TournamentOrganizationRepository.findByOrganizerUserId(
|
||||
user.id,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import {
|
|||
tournamentManagerData,
|
||||
} from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { i18next } from "~/modules/i18n/i18next.server";
|
||||
import {
|
||||
canDeleteCalendarEvent,
|
||||
canEditCalendarEvent,
|
||||
|
|
@ -42,7 +41,6 @@ import {
|
|||
notFoundIfFalsy,
|
||||
validate,
|
||||
} from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
CALENDAR_PAGE,
|
||||
calendarEditPage,
|
||||
|
|
@ -55,6 +53,7 @@ import {
|
|||
userPage,
|
||||
} from "~/utils/urls";
|
||||
import { actualNumber, id } from "~/utils/zod";
|
||||
import { metaTags } from "../../../utils/remix";
|
||||
import { Tags } from "../components/Tags";
|
||||
|
||||
import "~/styles/calendar-event.css";
|
||||
|
|
@ -103,10 +102,13 @@ export const meta: MetaFunction = (args) => {
|
|||
|
||||
if (!data) return [];
|
||||
|
||||
return [
|
||||
{ title: data.title },
|
||||
{ name: "description", content: data.event.description },
|
||||
];
|
||||
return metaTags({
|
||||
title: data.event.name,
|
||||
location: args.location,
|
||||
description:
|
||||
data.event.description ??
|
||||
`Splatoon competitive event hosted on ${resolveBaseUrl(data.event.bracketUrl)}`,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
@ -131,8 +133,7 @@ export const handle: SendouRouteHandle = {
|
|||
},
|
||||
};
|
||||
|
||||
export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
const t = await i18next.getFixedT(request);
|
||||
export const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const parsedParams = z
|
||||
.object({ id: z.preprocess(actualNumber, id) })
|
||||
.parse(params);
|
||||
|
|
@ -150,7 +151,6 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
|||
|
||||
return {
|
||||
event,
|
||||
title: makeTitle([event.name, t("pages.calendar")]),
|
||||
results: await CalendarRepository.findResultsByEventId(parsedParams.id),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MetaFunction, SerializeFrom } from "@remix-run/node";
|
||||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { Form, useFetcher, useLoaderData } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import Compressor from "compressorjs";
|
||||
|
|
@ -53,16 +53,20 @@ import { Tags } from "../components/Tags";
|
|||
import "~/styles/calendar-new.css";
|
||||
import "~/styles/maps.css";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import { action } from "../actions/calendar.new.server";
|
||||
import { loader } from "../loaders/calendar.new.server";
|
||||
export { loader, action };
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader> | null;
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
|
||||
if (!data) return [];
|
||||
const what = args.data.isAddingTournament ? "tournament" : "calendar event";
|
||||
|
||||
return [{ title: data.title }];
|
||||
return metaTags({
|
||||
title: args.data.eventToEdit ? `Editing ${what}` : `New ${what}`,
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import { getUserId } from "~/features/auth/core/user.server";
|
|||
import { currentSeason } from "~/features/mmr/season";
|
||||
import { HACKY_resolvePicture } from "~/features/tournament/tournament-utils";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { i18next } from "~/modules/i18n/i18next.server";
|
||||
import { joinListToNaturalString } from "~/utils/arrays";
|
||||
import {
|
||||
databaseTimestampToDate,
|
||||
|
|
@ -32,7 +31,6 @@ import {
|
|||
weekNumberToDate,
|
||||
} from "~/utils/dates";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
import {
|
||||
CALENDAR_PAGE,
|
||||
|
|
@ -49,6 +47,7 @@ import type {
|
|||
CalendarEventTag,
|
||||
PersistedCalendarEventTag,
|
||||
} from "../../../db/types";
|
||||
import { metaTags } from "../../../utils/remix";
|
||||
import * as CalendarRepository from "../CalendarRepository.server";
|
||||
import { calendarEventTagSchema } from "../actions/calendar.new.server";
|
||||
import { CALENDAR_EVENT } from "../calendar-constants";
|
||||
|
|
@ -62,17 +61,26 @@ export const meta: MetaFunction = (args) => {
|
|||
|
||||
if (!data) return [];
|
||||
|
||||
return [
|
||||
{ title: data.title },
|
||||
{
|
||||
name: "description",
|
||||
content: `${data.events.length} events happening during week ${
|
||||
data.displayedWeek
|
||||
} including ${joinListToNaturalString(
|
||||
data.events.slice(0, 3).map((e) => e.name),
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
const events = data.events.slice().sort((a, b) => {
|
||||
const aParticipants = a.participantCounts?.teams ?? 0;
|
||||
const bParticipants = b.participantCounts?.teams ?? 0;
|
||||
|
||||
if (aParticipants > bParticipants) return -1;
|
||||
if (aParticipants < bParticipants) return 1;
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return metaTags({
|
||||
title: "Calendar",
|
||||
ogTitle: "Splatoon competitive event calendar",
|
||||
location: args.location,
|
||||
description: `${data.events.length} events on sendou.ink happening during week ${
|
||||
data.displayedWeek
|
||||
} including ${joinListToNaturalString(
|
||||
events.slice(0, 3).map((e) => e.name),
|
||||
)}`,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
@ -99,7 +107,6 @@ const loaderTournamentsOnlySearchParamsSchema = z.object({
|
|||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const user = await getUserId(request);
|
||||
const t = await i18next.getFixedT(request);
|
||||
const url = new URL(request.url);
|
||||
|
||||
// separate from tags parse so they can fail independently
|
||||
|
|
@ -160,7 +167,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
eventsToReport: user
|
||||
? await CalendarRepository.eventsToReport(user.id)
|
||||
: [],
|
||||
title: makeTitle([`Week ${displayedWeek}`, t("pages.calendar")]),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,18 +3,20 @@ import * as React from "react";
|
|||
import { Trans } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Main } from "~/components/Main";
|
||||
import { useSetTitle } from "~/hooks/useSetTitle";
|
||||
import { languages } from "~/modules/i18n/config";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
GITHUB_CONTRIBUTORS_URL,
|
||||
RHODESMAS_FREESOUND_PROFILE_URL,
|
||||
SPLATOON_3_INK,
|
||||
} from "~/utils/urls";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: makeTitle("Contributions") }];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Contributions",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
@ -95,7 +97,6 @@ const TRANSLATORS: Array<{
|
|||
|
||||
export default function ContributionsPage() {
|
||||
const { t } = useTranslation(["common", "contributions"]);
|
||||
useSetTitle(t("common:pages.contributors"));
|
||||
|
||||
return (
|
||||
<Main>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Main } from "~/components/Main";
|
||||
import { useSetTitle } from "~/hooks/useSetTitle";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
|
||||
import "~/styles/faq.css";
|
||||
|
||||
const AMOUNT_OF_QUESTIONS = 9;
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: makeTitle("FAQ") },
|
||||
{ name: "description", content: "Frequently asked questions" },
|
||||
];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "FAQ",
|
||||
description: "Frequently asked questions",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
@ -22,7 +22,6 @@ export const handle: SendouRouteHandle = {
|
|||
|
||||
export default function FAQPage() {
|
||||
const { t } = useTranslation(["faq", "common"]);
|
||||
useSetTitle(t("common:pages.faq"));
|
||||
|
||||
return (
|
||||
<Main className="stack md">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { Main } from "~/components/Main";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: makeTitle("Privacy Policy") }];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Privacy Policy",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export default function PrivacyPolicyPage() {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ import { Badge } from "~/components/Badge";
|
|||
import { LinkButton } from "~/components/Button";
|
||||
import { Main } from "~/components/Main";
|
||||
import { CheckmarkIcon } from "~/components/icons/Checkmark";
|
||||
import { useSetTitle } from "~/hooks/useSetTitle";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import {
|
||||
PATREON_HOW_TO_CONNECT_DISCORD_URL,
|
||||
SENDOU_INK_PATREON_URL,
|
||||
|
|
@ -16,8 +15,12 @@ import { SendouPopover } from "../../../components/elements/Popover";
|
|||
|
||||
import "../support.css";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: makeTitle("Support") }];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Support",
|
||||
description: "Support Sendou's work on Patreon and get perks on sendou.ink",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
// 1 = support
|
||||
|
|
@ -108,7 +111,6 @@ const PERKS = [
|
|||
|
||||
export default function SupportPage() {
|
||||
const { t } = useTranslation();
|
||||
useSetTitle(t("pages.support"));
|
||||
|
||||
return (
|
||||
<Main className="stack lg">
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import {
|
|||
currentSeason,
|
||||
} from "~/features/mmr/season";
|
||||
import type { SkillTierInterval } from "~/features/mmr/tiered.server";
|
||||
import { i18next } from "~/modules/i18n/i18next.server";
|
||||
import {
|
||||
type MainWeaponId,
|
||||
type RankedModeShort,
|
||||
|
|
@ -28,8 +27,8 @@ import {
|
|||
} from "~/modules/in-game-lists";
|
||||
import { rankedModesShort } from "~/modules/in-game-lists/modes";
|
||||
import { cache, ttl } from "~/utils/cache.server";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
LEADERBOARDS_PAGE,
|
||||
navIconUrl,
|
||||
|
|
@ -75,14 +74,13 @@ export const meta: MetaFunction = (args) => {
|
|||
|
||||
if (!data) return [];
|
||||
|
||||
return [
|
||||
{ title: data.title },
|
||||
{
|
||||
name: "description",
|
||||
content:
|
||||
"Leaderboards of top Splatoon players ranked by their X Power and tournament results",
|
||||
},
|
||||
];
|
||||
return metaTags({
|
||||
title: "Leaderboards",
|
||||
ogTitle: "Splatoon leaderboards",
|
||||
description:
|
||||
"Leaderboards of top Splatoon players ranked by their X Battle placements as well as tournament and SendouQ results. Categories per weapon and mode.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
const TYPE_SEARCH_PARAM_KEY = "type";
|
||||
|
|
@ -90,7 +88,6 @@ const SEASON_SEARCH_PARAM_KEY = "season";
|
|||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const user = await getUser(request);
|
||||
const t = await i18next.getFixedT(request);
|
||||
const unvalidatedType = new URL(request.url).searchParams.get(
|
||||
TYPE_SEARCH_PARAM_KEY,
|
||||
);
|
||||
|
|
@ -159,7 +156,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
: type.startsWith("XP-WEAPON")
|
||||
? weaponXPLeaderboard(Number(type.split("-")[2]) as MainWeaponId)
|
||||
: null,
|
||||
title: makeTitle(t("pages.leaderboards")),
|
||||
season,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import { SubmitButton } from "~/components/SubmitButton";
|
|||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useSearchParamStateEncoder } from "~/hooks/useSearchParamState";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
import { LFG_PAGE, navIconUrl } from "~/utils/urls";
|
||||
import { LFGAddFilterButton } from "../components/LFGAddFilterButton";
|
||||
|
|
@ -39,8 +39,14 @@ export const handle: SendouRouteHandle = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: makeTitle("Looking for group") }];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "LFG",
|
||||
ogTitle: "Splatoon LFG (looking for players, teams & coaches)",
|
||||
description:
|
||||
"Find people to play Splatoon with. Create a post or browse existing ones. For looking players, teams, scrim partners and coaches alike.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export type LFGLoaderData = SerializeFrom<typeof loader>;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useTranslation } from "react-i18next";
|
||||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { Main } from "~/components/Main";
|
||||
import { DiscordIcon } from "~/components/icons/Discord";
|
||||
import { YouTubeIcon } from "~/components/icons/YouTube";
|
||||
import { useSetTitle } from "~/hooks/useSetTitle";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { LINKS_PAGE, navIconUrl } from "~/utils/urls";
|
||||
import links from "../links.json";
|
||||
|
|
@ -15,10 +15,17 @@ export const handle: SendouRouteHandle = {
|
|||
}),
|
||||
};
|
||||
|
||||
export default function LinksPage() {
|
||||
const { t } = useTranslation(["common"]);
|
||||
useSetTitle(t("common:pages.links"));
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Links",
|
||||
ogTitle: "Splatoon link collection",
|
||||
description:
|
||||
"Collection of useful Splatoon guides, Discord servers and other resources.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export default function LinksPage() {
|
||||
return (
|
||||
<Main>
|
||||
<div className="stack md">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
import type {
|
||||
LoaderFunctionArgs,
|
||||
MetaFunction,
|
||||
SerializeFrom,
|
||||
} from "@remix-run/node";
|
||||
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
||||
import type { ShouldRevalidateFunction } from "@remix-run/react";
|
||||
import { Link, useLoaderData, useSearchParams } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
|
|
@ -16,11 +12,9 @@ import { EditIcon } from "~/components/icons/Edit";
|
|||
import type { CalendarEvent } from "~/db/types";
|
||||
import { getUserId } from "~/features/auth/core/user.server";
|
||||
import * as CalendarRepository from "~/features/calendar/CalendarRepository.server";
|
||||
import { i18next } from "~/modules/i18n/i18next.server";
|
||||
import { type ModeWithStage, stageIds } from "~/modules/in-game-lists";
|
||||
import invariant from "~/utils/invariant";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
MAPS_URL,
|
||||
calendarEventPage,
|
||||
|
|
@ -33,6 +27,7 @@ import { mapPoolToNonEmptyModes } from "../core/map-list-generator/utils";
|
|||
import { MapPool } from "../core/map-pool";
|
||||
import "~/styles/maps.css";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
||||
const AMOUNT_OF_MAPS_IN_MAP_LIST = stageIds.length * 2;
|
||||
|
||||
|
|
@ -44,11 +39,13 @@ export const shouldRevalidate: ShouldRevalidateFunction = ({ nextUrl }) => {
|
|||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader> | null;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [{ title: data.title }];
|
||||
return metaTags({
|
||||
title: "Map List Generator",
|
||||
ogTitle: "Splatoon 3 map list generator",
|
||||
description:
|
||||
"Generate a map list based on maps you choose or a tournament's map pool.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
@ -64,7 +61,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
const user = await getUserId(request);
|
||||
const url = new URL(request.url);
|
||||
const calendarEventId = url.searchParams.get("eventId");
|
||||
const t = await i18next.getFixedT(request);
|
||||
|
||||
const event = calendarEventId
|
||||
? await CalendarRepository.findById({
|
||||
|
|
@ -84,7 +80,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
recentEventsWithMapPools: user
|
||||
? await CalendarRepository.findRecentMapPoolsByAuthorId(user.id)
|
||||
: undefined,
|
||||
title: makeTitle([t("pages.maps")]),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,20 @@
|
|||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { lazy } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useSetTitle } from "~/hooks/useSetTitle";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { PLANNER_URL, navIconUrl } from "~/utils/urls";
|
||||
|
||||
import "../plans.css";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: makeTitle("Planner") },
|
||||
{
|
||||
name: "description",
|
||||
content:
|
||||
"Make the perfect Splatoon 3 battle plans by drawing on maps and adding weapon images",
|
||||
},
|
||||
];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Map Planner",
|
||||
ogTitle: "Splatoon 3 Map planner",
|
||||
description:
|
||||
"Make perfect Splatoon 3 battle plans by drawing on maps and adding weapon images",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
@ -32,9 +29,7 @@ export const handle: SendouRouteHandle = {
|
|||
const Planner = lazy(() => import("~/features/map-planner/components/Planner"));
|
||||
|
||||
export default function MapPlannerPage() {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const isMounted = useIsMounted();
|
||||
useSetTitle(t("common:pages.plans"));
|
||||
|
||||
if (!isMounted) return <div className="plans__placeholder" />;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { Label } from "~/components/Label";
|
|||
import { Main } from "~/components/Main";
|
||||
import type { AnyWeapon, DamageType } from "~/features/build-analyzer";
|
||||
import { possibleApValues } from "~/features/build-analyzer";
|
||||
import { useSetTitle } from "~/hooks/useSetTitle";
|
||||
import {
|
||||
BIG_BUBBLER_ID,
|
||||
BOOYAH_BOMB_ID,
|
||||
|
|
@ -38,7 +37,9 @@ import {
|
|||
import { useObjectDamage } from "../calculator-hooks";
|
||||
import type { DamageReceiver } from "../calculator-types";
|
||||
import "../calculator.css";
|
||||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
||||
export const CURRENT_PATCH = "9.2";
|
||||
|
||||
|
|
@ -53,6 +54,16 @@ export const handle: SendouRouteHandle = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Object Damage Calculator",
|
||||
ogTitle: "Splatoon 3 object damage calculator",
|
||||
description:
|
||||
"Calculate how much damage weapons do to objects in Splatoon 3. The list of objects includes Crab Tank, Big Bubbler, Splash Wall, Rainmaker shield and more.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export default function ObjectDamagePage() {
|
||||
const { t } = useTranslation(["analyzer"]);
|
||||
const {
|
||||
|
|
@ -240,7 +251,6 @@ function DamageReceiversGrid({
|
|||
abilityPoints: string;
|
||||
}): JSX.Element {
|
||||
const { t } = useTranslation(["weapons", "analyzer", "common"]);
|
||||
useSetTitle(t("common:pages.object-damage-calculator"));
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -32,24 +32,24 @@ import {
|
|||
} from "~/permissions";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import {
|
||||
badRequestIfFalsy,
|
||||
parseRequestPayload,
|
||||
validate,
|
||||
} from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { userPage } from "~/utils/urls";
|
||||
import { _action, actualNumber } from "~/utils/zod";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: makeTitle("Plus Server suggestions") },
|
||||
{
|
||||
name: "description",
|
||||
content: "This month's suggestions for +1, +2 and +3.",
|
||||
},
|
||||
];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Plus Server suggestions",
|
||||
ogTitle: "Plus Server suggestions",
|
||||
description:
|
||||
"This season's suggestions to the Plus Server (+1, +2 and +3).",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
const suggestionActionSchema = z.union([
|
||||
|
|
|
|||
|
|
@ -11,26 +11,20 @@ import * as PlusVotingRepository from "~/features/plus-voting/PlusVotingReposito
|
|||
import { lastCompletedVoting } from "~/features/plus-voting/core";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { roundToNDecimalPlaces } from "~/utils/number";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { PLUS_SERVER_DISCORD_URL, userPage } from "~/utils/urls";
|
||||
import { isAtLeastFiveDollarTierPatreon } from "~/utils/users";
|
||||
|
||||
import "~/styles/plus-history.css";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader>;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [
|
||||
{ title: makeTitle("Plus Server voting history") },
|
||||
{
|
||||
name: "description",
|
||||
content: `Plus Server voting results for ${
|
||||
data.lastCompletedVoting.month + 1
|
||||
}/${data.lastCompletedVoting.year}`,
|
||||
},
|
||||
];
|
||||
return metaTags({
|
||||
title: "Plus Server voting results",
|
||||
ogTitle: "Plus Server voting results",
|
||||
description:
|
||||
"Plus Server (+1, +2 and +3) voting results for the latest season.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
|
|
|
|||
|
|
@ -23,14 +23,17 @@ import {
|
|||
import { isVotingActive } from "~/features/plus-voting/core/voting-time";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import { badRequestIfFalsy, parseRequestPayload } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { assertType, assertUnreachable } from "~/utils/types";
|
||||
import { safeJSONParse } from "~/utils/zod";
|
||||
import { PlusSuggestionComments } from "../../plus-suggestions/routes/plus.suggestions";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: makeTitle("Plus Server voting") }];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Plus Server Voting",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
const voteSchema = z.object({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { RadioGroup } from "@headlessui/react";
|
||||
import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import type {
|
||||
ActionFunctionArgs,
|
||||
LoaderFunctionArgs,
|
||||
MetaFunction,
|
||||
} from "@remix-run/node";
|
||||
import { useFetcher, useLoaderData } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
|
|
@ -53,6 +57,7 @@ import {
|
|||
import { settingsActionSchema } from "../q-settings-schemas.server";
|
||||
import "../q-settings.css";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["q"],
|
||||
|
|
@ -70,6 +75,13 @@ export const handle: SendouRouteHandle = {
|
|||
],
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "SendouQ - Settings",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const user = await requireUserId(request);
|
||||
const data = await parseRequestPayload({
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { Link, useLoaderData } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
|
|
@ -8,6 +9,7 @@ import { useAutoRerender } from "~/hooks/useAutoRerender";
|
|||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { twitchThumbnailUrlToSrc } from "~/modules/twitch/utils";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { FAQ_PAGE, sendouQMatchPage, twitchUrl, userPage } from "~/utils/urls";
|
||||
import { cachedStreams } from "../core/streams.server";
|
||||
|
|
@ -18,6 +20,14 @@ export const handle: SendouRouteHandle = {
|
|||
i18n: ["q"],
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "SendouQ - Streams",
|
||||
description: "Streams of SendouQ matches in progress.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const loader = async () => {
|
||||
return {
|
||||
streams: await cachedStreams(),
|
||||
|
|
|
|||
|
|
@ -8,12 +8,21 @@ import {
|
|||
TIERS_PAGE,
|
||||
navIconUrl,
|
||||
} from "~/utils/urls";
|
||||
|
||||
import "../q.css";
|
||||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { Button } from "~/components/Button";
|
||||
import { Image } from "~/components/Image";
|
||||
import { MATCHES_COUNT_NEEDED_FOR_LEADERBOARD } from "~/features/leaderboards/leaderboards-constants";
|
||||
import { USER_LEADERBOARD_MIN_ENTRIES_FOR_LEVIATHAN } from "~/features/mmr/mmr-constants";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "SendouQ - Info",
|
||||
description: "SendouQ guide and information.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export default function SendouQInfoPage() {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -27,13 +27,13 @@ import { useAutoRefresh } from "~/hooks/useAutoRefresh";
|
|||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useWindowSize } from "~/hooks/useWindowSize";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import {
|
||||
type SendouRouteHandle,
|
||||
parseRequestPayload,
|
||||
validate,
|
||||
} from "~/utils/remix.server";
|
||||
import { errorIsSqliteForeignKeyConstraintFailure } from "~/utils/sql";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import {
|
||||
SENDOUQ_LOOKING_PAGE,
|
||||
|
|
@ -95,8 +95,11 @@ export const handle: SendouRouteHandle = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: makeTitle("SendouQ") }];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "SendouQ - Matchmaking",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
// this function doesn't throw normally because we are assuming
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ import {
|
|||
parseRequestPayload,
|
||||
validate,
|
||||
} from "~/utils/remix.server";
|
||||
import { inGameNameWithoutDiscriminator, makeTitle } from "~/utils/strings";
|
||||
import { inGameNameWithoutDiscriminator } from "~/utils/strings";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import {
|
||||
|
|
@ -113,25 +113,22 @@ import { reportedWeaponsByMatchId } from "../queries/reportedWeaponsByMatchId.se
|
|||
import { setGroupAsInactive } from "../queries/setGroupAsInactive.server";
|
||||
import "../q.css";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader> | null;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
title: makeTitle(`SendouQ Match #${data.match.id}`),
|
||||
},
|
||||
{
|
||||
name: "description",
|
||||
content: `${joinListToNaturalString(
|
||||
data.groupAlpha.members.map((m) => m.username),
|
||||
)} vs. ${joinListToNaturalString(
|
||||
data.groupBravo.members.map((m) => m.username),
|
||||
)}`,
|
||||
},
|
||||
];
|
||||
return metaTags({
|
||||
title: `SendouQ - Match #${data.match.id}`,
|
||||
description: `${joinListToNaturalString(
|
||||
data.groupAlpha.members.map((m) => m.username),
|
||||
)} vs. ${joinListToNaturalString(
|
||||
data.groupBravo.members.map((m) => m.username),
|
||||
)}`,
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ import * as QMatchRepository from "~/features/sendouq-match/QMatchRepository.ser
|
|||
import * as QRepository from "~/features/sendouq/QRepository.server";
|
||||
import { useAutoRefresh } from "~/hooks/useAutoRefresh";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { parseRequestPayload, validate } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import {
|
||||
SENDOUQ_LOOKING_PAGE,
|
||||
|
|
@ -47,8 +47,11 @@ export const handle: SendouRouteHandle = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: makeTitle("SendouQ") }];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "SendouQ - Preparing Group",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export type SendouQPreparingAction = typeof action;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import type { MetaFunction } from "@remix-run/react";
|
||||
import { Main } from "~/components/Main";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [{ title: makeTitle("SendouQ Rules") }];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "SendouQ - Rules",
|
||||
description: "Rules everyone participating in SendouQ has to follow.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export default function SendouqRules() {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import {
|
|||
parseRequestPayload,
|
||||
validate,
|
||||
} from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import {
|
||||
LEADERBOARDS_PAGE,
|
||||
|
|
@ -67,8 +66,8 @@ import { addMember } from "../queries/addMember.server";
|
|||
import { deleteLikesByGroupId } from "../queries/deleteLikesByGroupId.server";
|
||||
import { findCurrentGroupByUserId } from "../queries/findCurrentGroupByUserId.server";
|
||||
import { findGroupByInviteCode } from "../queries/findGroupByInviteCode.server";
|
||||
|
||||
import "../q.css";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["q"],
|
||||
|
|
@ -79,15 +78,13 @@ export const handle: SendouRouteHandle = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: makeTitle("SendouQ") },
|
||||
{
|
||||
name: "description",
|
||||
content:
|
||||
"Splatoon 3 competitive ladder. Join by yourself or with your team and play ranked matches.",
|
||||
},
|
||||
];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "SendouQ",
|
||||
description:
|
||||
"Splatoon 3 competitive ladder. Join by yourself or with your team and play ranked matches.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
const validateCanJoinQ = async (user: { id: number; discordId: string }) => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TierImage } from "~/components/Image";
|
||||
|
|
@ -10,8 +11,18 @@ import {
|
|||
import { ordinalToSp } from "~/features/mmr/mmr-utils";
|
||||
import { currentOrPreviousSeason } from "~/features/mmr/season";
|
||||
import { userSkills } from "~/features/mmr/tiered.server";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "SendouQ - Tiers",
|
||||
description:
|
||||
"Information about the tiers in SendouQ. From Leviathan+ to Iron.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["q"],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import type { MetaFunction } from "@remix-run/node";
|
||||
import { useNavigate, useSearchParams } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import { Theme, useTheme } from "~/features/theme/core/provider";
|
||||
import { languages } from "~/modules/i18n/config";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { SETTINGS_PAGE, navIconUrl } from "~/utils/urls";
|
||||
|
||||
|
|
@ -29,6 +31,13 @@ export default function SettingsPage() {
|
|||
);
|
||||
}
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Settings",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
function LanguageSelector() {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const { i18n } = useTranslation();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import type { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import type { UserWithPlusTier } from "~/db/types";
|
||||
import { getUserId } from "~/features/auth/core/user.server";
|
||||
import { i18next } from "~/modules/i18n/i18next.server";
|
||||
import { sumArray } from "~/utils/number";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import * as TeamRepository from "../TeamRepository.server";
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const user = await getUserId(request);
|
||||
const t = await i18next.getFixedT(request);
|
||||
|
||||
const unsortedTeams = await TeamRepository.findAllUndisbanded();
|
||||
|
||||
|
|
@ -47,7 +44,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
});
|
||||
|
||||
return {
|
||||
title: makeTitle(t("pages.t")),
|
||||
teams,
|
||||
teamMemberOfCount: user
|
||||
? teams.filter((team) => team.members.some((m) => m.id === user.id))
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { Main } from "~/components/Main";
|
|||
import { SubmitButton } from "~/components/SubmitButton";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
TEAM_SEARCH_PAGE,
|
||||
navIconUrl,
|
||||
|
|
@ -25,13 +24,15 @@ import { loader } from "../loaders/t.$customUrl.edit.server";
|
|||
import { TEAM } from "../team-constants";
|
||||
import { canAddCustomizedColors, isTeamOwner } from "../team-utils";
|
||||
import "../team.css";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
|
||||
export { action, loader };
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
if (!data) return [];
|
||||
|
||||
return [{ title: makeTitle(data.team.name) }];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Editing team",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { SendouSwitch } from "~/components/elements/Switch";
|
|||
import { TrashIcon } from "~/components/icons/Trash";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
TEAM_SEARCH_PAGE,
|
||||
joinTeamPage,
|
||||
|
|
@ -26,15 +25,17 @@ import type * as TeamRepository from "../TeamRepository.server";
|
|||
import { TEAM_MEMBER_ROLES } from "../team-constants";
|
||||
import { isTeamFull } from "../team-utils";
|
||||
import "../team.css";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import { action } from "../actions/t.$customUrl.roster.server";
|
||||
import { loader } from "../loaders/t.$customUrl.roster.server";
|
||||
|
||||
export { loader, action };
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
if (!data) return [];
|
||||
|
||||
return [{ title: makeTitle(data.team.name) }];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Managing team roster",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import { useUser } from "~/features/auth/core/user";
|
|||
import { isAdmin } from "~/permissions";
|
||||
import { removeDuplicates } from "~/utils/arrays";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
TEAM_SEARCH_PAGE,
|
||||
bskyUrl,
|
||||
|
|
@ -34,15 +33,26 @@ import { action } from "../actions/t.$customUrl.server";
|
|||
import { loader } from "../loaders/t.$customUrl.server";
|
||||
import { isTeamManager, isTeamMember, resolveNewOwner } from "../team-utils";
|
||||
import "../team.css";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
export { action, loader };
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
if (!data) return [];
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
|
||||
return [
|
||||
{ title: makeTitle(data.team.name) },
|
||||
{ name: "description", content: data.team.bio },
|
||||
];
|
||||
return metaTags({
|
||||
title: args.data.team.name,
|
||||
description: args.data.team.bio ?? undefined,
|
||||
location: args.location,
|
||||
image: args.data.team.avatarSrc
|
||||
? {
|
||||
url: userSubmittedImage(args.data.team.avatarSrc),
|
||||
dimensions: {
|
||||
width: 124,
|
||||
height: 124,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { SearchIcon } from "~/components/icons/Search";
|
|||
import { useUser } from "~/features/auth/core/user";
|
||||
import { usePagination } from "~/hooks/usePagination";
|
||||
import { joinListToNaturalString } from "~/utils/arrays";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import {
|
||||
TEAM_SEARCH_PAGE,
|
||||
|
|
@ -36,10 +37,14 @@ import { action } from "../actions/t.server";
|
|||
import { loader } from "../loaders/t.server";
|
||||
export { loader, action };
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
if (!data) return [];
|
||||
|
||||
return [{ title: data.title }];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "Team Search",
|
||||
ogTitle: "Splatoon team search",
|
||||
description:
|
||||
"List of all teams on sendou.ink and their members. Search for teams by name or member name.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,9 @@ import type {
|
|||
import { Link, useLoaderData } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Main } from "~/components/Main";
|
||||
import { i18next } from "~/modules/i18n/i18next.server";
|
||||
import { removeDuplicates } from "~/utils/arrays";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import { type SendouRouteHandle, notFoundIfFalsy } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
navIconUrl,
|
||||
topSearchPage,
|
||||
|
|
@ -44,30 +43,39 @@ export const handle: SendouRouteHandle = {
|
|||
},
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader> | null;
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
|
||||
if (!data) return [];
|
||||
const aliasesStr =
|
||||
args.data.names.aliases.length > 0
|
||||
? ` (Aliases: ${args.data.names.aliases.join(", ")})`
|
||||
: "";
|
||||
|
||||
return [
|
||||
{ title: data.title },
|
||||
{
|
||||
name: "description",
|
||||
content: `Splatoon 3 X Battle for the player ${data.placements[0].name}`,
|
||||
},
|
||||
];
|
||||
return metaTags({
|
||||
title: `${args.data.names.primary} X Battle Top 500 Placements`,
|
||||
description: `Splatoon 3 X Battle results for the player ${args.data.names.primary}${aliasesStr}`,
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
export const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||
const placements = notFoundIfFalsy(
|
||||
findPlacementsByPlayerId(Number(params.id)),
|
||||
);
|
||||
|
||||
const t = await i18next.getFixedT(request);
|
||||
const primaryName = placements[0].name;
|
||||
const aliases = removeDuplicates(
|
||||
placements
|
||||
.map((placement) => placement.name)
|
||||
.filter((name) => name !== primaryName),
|
||||
);
|
||||
|
||||
return {
|
||||
placements,
|
||||
title: makeTitle([placements[0].name, t("pages.xsearch")]),
|
||||
names: {
|
||||
primary: primaryName,
|
||||
aliases,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -75,13 +83,6 @@ export default function XSearchPlayerPage() {
|
|||
const { t } = useTranslation(["common"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
const firstName = data.placements[0].name;
|
||||
const aliases = removeDuplicates(
|
||||
data.placements
|
||||
.map((placement) => placement.name)
|
||||
.filter((name) => name !== firstName),
|
||||
);
|
||||
|
||||
const hasUserLinked = Boolean(data.placements[0].discordId);
|
||||
|
||||
return (
|
||||
|
|
@ -89,15 +90,15 @@ export default function XSearchPlayerPage() {
|
|||
<div>
|
||||
<h2 className="text-lg">
|
||||
{hasUserLinked ? (
|
||||
<Link to={userPage(data.placements[0])}>{firstName}</Link>
|
||||
<Link to={userPage(data.placements[0])}>{data.names.primary}</Link>
|
||||
) : (
|
||||
<>{firstName}</>
|
||||
<>{data.names.primary}</>
|
||||
)}{" "}
|
||||
{t("common:xsearch.placements")}
|
||||
</h2>
|
||||
{aliases.length > 0 ? (
|
||||
{data.names.aliases.length > 0 ? (
|
||||
<div className="text-lighter text-sm">
|
||||
{t("common:xsearch.aliases")} {aliases.join(", ")}
|
||||
{t("common:xsearch.aliases")} {data.names.aliases.join(", ")}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,14 @@
|
|||
import type {
|
||||
LoaderFunctionArgs,
|
||||
MetaFunction,
|
||||
SerializeFrom,
|
||||
} from "@remix-run/node";
|
||||
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
||||
import { useLoaderData, useSearchParams } from "@remix-run/react";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Main } from "~/components/Main";
|
||||
import type { XRankPlacement } from "~/db/types";
|
||||
import { i18next } from "~/modules/i18n/i18next.server";
|
||||
import type { RankedModeShort } from "~/modules/in-game-lists";
|
||||
import { rankedModesShort } from "~/modules/in-game-lists/modes";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { navIconUrl, topSearchPage } from "~/utils/urls";
|
||||
import { PlacementsTable } from "../components/Placements";
|
||||
import { findPlacementsOfMonth } from "../queries/findPlacements.server";
|
||||
|
|
@ -31,21 +26,19 @@ export const handle: SendouRouteHandle = {
|
|||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader> | null;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [
|
||||
{ title: data.title },
|
||||
{ name: "description", content: "Splatoon 3 X Battle results" },
|
||||
];
|
||||
return metaTags({
|
||||
title: "X Battle Top 500 Placements",
|
||||
ogTitle: "Splatoon 3 X Battle Top 500 results browser",
|
||||
description:
|
||||
"Splatoon 3 X Battle results for the top 500 players for all the finished seasons in both Tentatek and Takoroka divisions.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const availableMonthYears = monthYears();
|
||||
const { month: latestMonth, year: latestYear } = availableMonthYears[0];
|
||||
|
||||
// #region parse URL params
|
||||
const url = new URL(request.url);
|
||||
const mode = (() => {
|
||||
const mode = url.searchParams.get("mode");
|
||||
|
|
@ -85,7 +78,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
|
||||
return latestYear;
|
||||
})();
|
||||
// #endregion
|
||||
|
||||
const placements = findPlacementsOfMonth({
|
||||
mode,
|
||||
|
|
@ -94,10 +86,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
year,
|
||||
});
|
||||
|
||||
const t = await i18next.getFixedT(request);
|
||||
|
||||
return {
|
||||
title: makeTitle(t("pages.xsearch")),
|
||||
placements,
|
||||
availableMonthYears,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import { useUser } from "~/features/auth/core/user";
|
|||
import { BadgeDisplay } from "~/features/badges/components/BadgeDisplay";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { databaseTimestampNow, databaseTimestampToDate } from "~/utils/dates";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
BLANK_IMAGE_URL,
|
||||
calendarEventPage,
|
||||
|
|
@ -34,34 +34,20 @@ import "../tournament-organization.css";
|
|||
import { loader } from "../loaders/org.$slug.server";
|
||||
export { loader };
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader>;
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
const title = makeTitle(data.organization.name);
|
||||
|
||||
return [
|
||||
{ title },
|
||||
{
|
||||
property: "og:title",
|
||||
content: title,
|
||||
},
|
||||
{
|
||||
property: "og:description",
|
||||
content: data.organization.description,
|
||||
},
|
||||
{
|
||||
property: "og:type",
|
||||
content: "website",
|
||||
},
|
||||
{
|
||||
property: "og:image",
|
||||
content: data.organization.avatarUrl
|
||||
? userSubmittedImage(data.organization.avatarUrl)
|
||||
: undefined,
|
||||
},
|
||||
];
|
||||
return metaTags({
|
||||
title: args.data.organization.name,
|
||||
location: args.location,
|
||||
description: args.data.organization.description ?? undefined,
|
||||
image: args.data.organization.avatarUrl
|
||||
? {
|
||||
url: userSubmittedImage(args.data.organization.avatarUrl),
|
||||
dimensions: { width: 124, height: 124 },
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
||||
import { Link, useLoaderData } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
|
@ -8,9 +8,13 @@ import { Placement } from "~/components/Placement";
|
|||
import { Redirect } from "~/components/Redirect";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { SendouPopover } from "~/components/elements/Popover";
|
||||
import type { TournamentDataTeam } from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import type {
|
||||
TournamentData,
|
||||
TournamentDataTeam,
|
||||
} from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import { tournamentTeamPageParamsSchema } from "~/features/tournament-bracket/tournament-bracket-schemas.server";
|
||||
import type { TournamentMaplistSource } from "~/modules/tournament-map-list-generator";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import { parseParams } from "~/utils/remix.server";
|
||||
import {
|
||||
teamPage,
|
||||
|
|
@ -18,6 +22,7 @@ import {
|
|||
tournamentPage,
|
||||
tournamentTeamPage,
|
||||
userPage,
|
||||
userSubmittedImage,
|
||||
} from "~/utils/urls";
|
||||
import { TeamWithRoster } from "../components/TeamWithRoster";
|
||||
import {
|
||||
|
|
@ -28,6 +33,29 @@ import {
|
|||
import { tournamentIdFromParams } from "../tournament-utils";
|
||||
import { useTournament } from "./to.$id";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
const tournamentData = (args.matches[1].data as any)
|
||||
?.tournament as TournamentData;
|
||||
if (!args.data || !tournamentData) return [];
|
||||
|
||||
const team = tournamentData.ctx.teams.find(
|
||||
(t) => t.id === args.data!.tournamentTeamId,
|
||||
)!;
|
||||
const teamLogoUrl = team.team?.logoUrl ?? team.pickupAvatarUrl;
|
||||
|
||||
return metaTags({
|
||||
title: `${team.name} @ ${tournamentData.ctx.name}`,
|
||||
description: `${team.name} roster (${team.members.map((m) => m.username).join(", ")}) and sets in ${tournamentData.ctx.name}.`,
|
||||
image: teamLogoUrl
|
||||
? {
|
||||
url: userSubmittedImage(teamLogoUrl),
|
||||
dimensions: { width: 124, height: 124 },
|
||||
}
|
||||
: undefined,
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const loader = ({ params }: LoaderFunctionArgs) => {
|
||||
const tournamentId = tournamentIdFromParams(params);
|
||||
const tournamentTeamId = parseParams({
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { useIsMounted } from "~/hooks/useIsMounted";
|
|||
import { isAdmin } from "~/permissions";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { removeMarkdown } from "~/utils/strings";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import {
|
||||
tournamentDivisionsPage,
|
||||
|
|
@ -31,6 +31,7 @@ import {
|
|||
tournamentRegisterPage,
|
||||
userSubmittedImage,
|
||||
} from "~/utils/urls";
|
||||
import { metaTags } from "../../../utils/remix";
|
||||
import { streamsByTournamentId } from "../core/streams.server";
|
||||
import {
|
||||
HACKY_resolvePicture,
|
||||
|
|
@ -55,39 +56,18 @@ export const meta: MetaFunction = (args) => {
|
|||
|
||||
if (!data) return [];
|
||||
|
||||
const title = makeTitle(data.tournament.ctx.name);
|
||||
|
||||
const ogImage = () => {
|
||||
if (
|
||||
!data.tournament.ctx.logoSrc ||
|
||||
data.tournament.ctx.logoSrc.startsWith("https")
|
||||
) {
|
||||
return data.tournament.ctx.logoSrc;
|
||||
}
|
||||
|
||||
// opengraph does not support relative urls
|
||||
return `${import.meta.env.VITE_SITE_DOMAIN}${data.tournament.ctx.logoSrc}`;
|
||||
};
|
||||
|
||||
return [
|
||||
{ title },
|
||||
{
|
||||
property: "og:title",
|
||||
content: title,
|
||||
return metaTags({
|
||||
title: data.tournament.ctx.name,
|
||||
description: data.tournament.ctx.description
|
||||
? removeMarkdown(data.tournament.ctx.description)
|
||||
: undefined,
|
||||
image: {
|
||||
url: data.tournament.ctx.logoSrc,
|
||||
dimensions: { width: 124, height: 124 },
|
||||
},
|
||||
{
|
||||
property: "og:description",
|
||||
content: data.tournament.ctx.description,
|
||||
},
|
||||
{
|
||||
property: "og:type",
|
||||
content: "website",
|
||||
},
|
||||
{
|
||||
property: "og:image",
|
||||
content: ogImage(),
|
||||
},
|
||||
];
|
||||
location: args.location,
|
||||
url: tournamentPage(data.tournament.ctx.id),
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import { SubNav, SubNavLink } from "~/components/SubNav";
|
|||
import { useUser } from "~/features/auth/core/user";
|
||||
import { getUserId } from "~/features/auth/core/user.server";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import { type SendouRouteHandle, notFoundIfFalsy } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
USER_SEARCH_PAGE,
|
||||
navIconUrl,
|
||||
|
|
@ -26,10 +26,14 @@ import {
|
|||
|
||||
import "~/styles/u.css";
|
||||
|
||||
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
||||
if (!data) return [];
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
|
||||
return [{ title: makeTitle(data.user.username) }];
|
||||
return metaTags({
|
||||
title: args.data.user.username,
|
||||
description: `${args.data.user.username}'s profile on sendou.ink including builds, tournament results, art and more.`,
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import type { LoaderFunctionArgs, SerializeFrom } from "@remix-run/node";
|
||||
import type {
|
||||
LoaderFunctionArgs,
|
||||
MetaFunction,
|
||||
SerializeFrom,
|
||||
} from "@remix-run/node";
|
||||
import { Link, useLoaderData, useSearchParams } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
|
@ -9,6 +13,7 @@ import { Input } from "~/components/Input";
|
|||
import { Main } from "~/components/Main";
|
||||
import { SearchIcon } from "~/components/icons/Search";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import {
|
||||
type SendouRouteHandle,
|
||||
parseSearchParams,
|
||||
|
|
@ -27,6 +32,14 @@ export const handle: SendouRouteHandle = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "User Search",
|
||||
description: "Search for sendou.ink users",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export type UserSearchLoaderData = SerializeFrom<typeof loader>;
|
||||
|
||||
const searchParamsSchema = z.object({
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import { useUser } from "~/features/auth/core/user";
|
|||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import { type SendouRouteHandle, notFoundIfFalsy } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
import {
|
||||
VODS_PAGE,
|
||||
|
|
@ -60,12 +60,15 @@ export const handle: SendouRouteHandle = {
|
|||
},
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader> | null;
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
if (!args.data) return [];
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [{ title: makeTitle(data.vod.title) }];
|
||||
return metaTags({
|
||||
title: args.data.vod.title,
|
||||
description:
|
||||
"Splatoon 3 VoD with timestamps to check out specific weapons as well as map and mode combinations.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const loader = ({ params }: LoaderFunctionArgs) => {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
import type {
|
||||
LoaderFunctionArgs,
|
||||
MetaFunction,
|
||||
SerializeFrom,
|
||||
} from "@remix-run/node";
|
||||
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
||||
import { useLoaderData, useSearchParams } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "~/components/Button";
|
||||
import { WeaponCombobox } from "~/components/Combobox";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import { i18next } from "~/modules/i18n/i18next.server";
|
||||
import { mainWeaponIds, modesShort, stageIds } from "~/modules/in-game-lists";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { VODS_PAGE, navIconUrl } from "~/utils/urls";
|
||||
import { VodListing } from "../components/VodListing";
|
||||
import { findVods } from "../queries/findVods.server";
|
||||
|
|
@ -29,16 +24,17 @@ export const handle: SendouRouteHandle = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = (args) => {
|
||||
const data = args.data as SerializeFrom<typeof loader> | null;
|
||||
|
||||
if (!data) return [];
|
||||
|
||||
return [{ title: data.title }];
|
||||
export const meta: MetaFunction<typeof loader> = (args) => {
|
||||
return metaTags({
|
||||
title: "VODs",
|
||||
ogTitle: "Splatoon 3 VODs (gameplay footage search)",
|
||||
description:
|
||||
"Search for Splatoon 3 VODs (gameplay footage) by mode, stage and/or weapon.",
|
||||
location: args.location,
|
||||
});
|
||||
};
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const t = await i18next.getFixedT(request);
|
||||
const url = new URL(request.url);
|
||||
|
||||
const limit = Number(url.searchParams.get("limit") ?? VODS_PAGE_BATCH_SIZE);
|
||||
|
|
@ -58,7 +54,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
|
||||
return {
|
||||
vods,
|
||||
title: makeTitle(t("pages.vods")),
|
||||
limit,
|
||||
hasMoreVods,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
import { useEffect } from "react";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
|
||||
/** Set title on mount. Used for showing a translated title on pages without loader. */
|
||||
export function useSetTitle(title: string) {
|
||||
useEffect(() => {
|
||||
document.title = makeTitle(title);
|
||||
}, [title]);
|
||||
}
|
||||
25
app/root.tsx
25
app/root.tsx
|
|
@ -43,8 +43,8 @@ import { useVisibilityChange } from "./hooks/useVisibilityChange";
|
|||
import { DEFAULT_LANGUAGE } from "./modules/i18n/config";
|
||||
import i18next, { i18nCookie } from "./modules/i18n/i18next.server";
|
||||
import type { Namespace } from "./modules/i18n/resources.server";
|
||||
import { isRevalidation } from "./utils/remix";
|
||||
import { COMMON_PREVIEW_IMAGE, SUSPENDED_PAGE } from "./utils/urls";
|
||||
import { isRevalidation, metaTags } from "./utils/remix";
|
||||
import { SUSPENDED_PAGE } from "./utils/urls";
|
||||
|
||||
import "nprogress/nprogress.css";
|
||||
import "~/styles/common.css";
|
||||
|
|
@ -64,19 +64,14 @@ export const shouldRevalidate: ShouldRevalidateFunction = (args) => {
|
|||
return Boolean(lang);
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "sendou.ink" },
|
||||
{
|
||||
name: "description",
|
||||
content:
|
||||
"Competitive Splatoon Hub featuring gear planner, event calendar, builds by top players, and more!",
|
||||
},
|
||||
{
|
||||
property: "og:image",
|
||||
content: COMMON_PREVIEW_IMAGE,
|
||||
},
|
||||
];
|
||||
export const meta: MetaFunction = (args) => {
|
||||
return metaTags({
|
||||
title: "sendou.ink",
|
||||
ogTitle: "sendou.ink - Competitive Splatoon Hub",
|
||||
location: args.location,
|
||||
description:
|
||||
"Sendou.ink is the home of competitive Splatoon featuring daily tournaments and a seasonal ladder. Variety of tools and the largest collection of builds by top players allow you to level up your skill in Splatoon 3.",
|
||||
});
|
||||
};
|
||||
|
||||
export type RootLoaderData = SerializeFrom<typeof loader>;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,122 @@
|
|||
import type { ShouldRevalidateFunctionArgs } from "@remix-run/react";
|
||||
import type {
|
||||
Location,
|
||||
ShouldRevalidateFunctionArgs,
|
||||
useLoaderData,
|
||||
} from "@remix-run/react";
|
||||
import { truncateBySentence } from "./strings";
|
||||
import { COMMON_PREVIEW_IMAGE } from "./urls";
|
||||
|
||||
export function isRevalidation(args: ShouldRevalidateFunctionArgs) {
|
||||
return (
|
||||
args.defaultShouldRevalidate && args.nextUrl.href === args.currentUrl.href
|
||||
);
|
||||
}
|
||||
|
||||
// https://remix.run/docs/en/main/start/future-flags#serializefrom
|
||||
export type SerializeFrom<T> = ReturnType<typeof useLoaderData<T>>;
|
||||
|
||||
interface OpenGraphArgs {
|
||||
/** Title as shown by the browser in the tab etc. Appended with "| sendou.ink"*/
|
||||
title: string;
|
||||
/** Title as shown when shared on Bluesky, Discord etc. Also used in search results. If omitted, "title" is used instead. */
|
||||
ogTitle?: string;
|
||||
/** Brief description of the page's contents used by search engines and social media sharing. If the description is over 300 characters long it is automatically truncated. */
|
||||
description?: string;
|
||||
location: Location;
|
||||
/** Optionally override location pathname. */
|
||||
url?: string;
|
||||
image?: {
|
||||
url: string;
|
||||
dimensions?: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const ROOT_URL = "https://sendou.ink";
|
||||
|
||||
export function metaTitle(args: Pick<OpenGraphArgs, "title" | "ogTitle">) {
|
||||
return [
|
||||
{
|
||||
title:
|
||||
args.title === "sendou.ink" ? args.title : `${args.title} | sendou.ink`,
|
||||
},
|
||||
{
|
||||
property: "og:title",
|
||||
content: args.ogTitle ?? args.title,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function metaTags(args: OpenGraphArgs) {
|
||||
const truncatedDescription = args.description
|
||||
? truncateBySentence(args.description, 300)
|
||||
: null;
|
||||
|
||||
const result = [
|
||||
...metaTitle(args),
|
||||
args.description
|
||||
? {
|
||||
name: "description",
|
||||
content: truncatedDescription,
|
||||
}
|
||||
: null,
|
||||
args.description
|
||||
? {
|
||||
property: "og:description",
|
||||
content: truncatedDescription,
|
||||
}
|
||||
: null,
|
||||
{
|
||||
property: "og:site_name",
|
||||
content: "sendou.ink",
|
||||
},
|
||||
{
|
||||
property: "og:type",
|
||||
content: "website",
|
||||
},
|
||||
{
|
||||
property: "og:url",
|
||||
content: `${ROOT_URL}${args.location.pathname}`,
|
||||
},
|
||||
{
|
||||
property: "og:image",
|
||||
content: (() => {
|
||||
if (args.image?.url.startsWith("http")) {
|
||||
return args.image.url;
|
||||
}
|
||||
|
||||
if (args.image) {
|
||||
return `${ROOT_URL}${args.image.url}`;
|
||||
}
|
||||
|
||||
return `${ROOT_URL}${COMMON_PREVIEW_IMAGE}`;
|
||||
})(),
|
||||
},
|
||||
].filter((val) => val !== null);
|
||||
|
||||
if (!args.image) {
|
||||
result.push({
|
||||
property: "og:image:width",
|
||||
content: "1920",
|
||||
});
|
||||
|
||||
result.push({
|
||||
property: "og:image:height",
|
||||
content: "1080",
|
||||
});
|
||||
} else if (args.image.dimensions) {
|
||||
result.push({
|
||||
property: "og:image:width",
|
||||
content: String(args.image.dimensions.width),
|
||||
});
|
||||
|
||||
result.push({
|
||||
property: "og:image:height",
|
||||
content: String(args.image.dimensions.height),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
import { pathnameFromPotentialURL } from "./strings";
|
||||
import { pathnameFromPotentialURL, truncateBySentence } from "./strings";
|
||||
|
||||
describe("pathnameFromPotentialURL()", () => {
|
||||
test("Resolves path name from valid URL", () => {
|
||||
|
|
@ -12,3 +12,35 @@ describe("pathnameFromPotentialURL()", () => {
|
|||
expect(pathnameFromPotentialURL("sendouc")).toBe("sendouc");
|
||||
});
|
||||
});
|
||||
|
||||
describe("truncateBySentence()", () => {
|
||||
test("Truncates text by sentence within max length", () => {
|
||||
const text = "This is the first sentence. This is the second sentence.";
|
||||
expect(truncateBySentence(text, 30)).toBe("This is the first sentence.");
|
||||
});
|
||||
|
||||
test("Returns original text if no sentences fit within max length", () => {
|
||||
const text = "This is a very long sentence that exceeds the max length.";
|
||||
expect(truncateBySentence(text, 10)).toBe("This is a");
|
||||
});
|
||||
|
||||
test("Returns original text if it is shorter than max length", () => {
|
||||
const text = "Short text.";
|
||||
expect(truncateBySentence(text, 50)).toBe("Short text.");
|
||||
});
|
||||
|
||||
test("Handles no senteces", () => {
|
||||
const text = "One two three four five six seven eight nine ten";
|
||||
expect(truncateBySentence(text, 10)).toBe("One two th");
|
||||
});
|
||||
|
||||
test("Truncates text by sentence with newline characters", () => {
|
||||
const text = "This is the first sentence\nThis is the second sentence";
|
||||
expect(truncateBySentence(text, 30)).toBe("This is the first sentence");
|
||||
});
|
||||
|
||||
test("Handles text with multiple newline characters", () => {
|
||||
const text = "First line\nSecond line\nThird line";
|
||||
expect(truncateBySentence(text, 20)).toBe("First line");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,10 +5,6 @@ export function inGameNameWithoutDiscriminator(inGameName: string) {
|
|||
return inGameName.split("#")[0];
|
||||
}
|
||||
|
||||
export function makeTitle(title: string | string[]) {
|
||||
return `${Array.isArray(title) ? title.join(" | ") : title} | sendou.ink`;
|
||||
}
|
||||
|
||||
export function semiRandomId() {
|
||||
return String(Math.random());
|
||||
}
|
||||
|
|
@ -68,3 +64,62 @@ export function pathnameFromPotentialURL(maybeUrl: string) {
|
|||
return maybeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
export function truncateBySentence(value: string, max: number) {
|
||||
if (value.length <= max) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const sentences = value.match(/[^.!?\n]+[.!?\n]*/g) || [];
|
||||
let result = "";
|
||||
|
||||
for (const sentence of sentences) {
|
||||
if ((result + sentence).length > max) {
|
||||
break;
|
||||
}
|
||||
result += sentence;
|
||||
}
|
||||
|
||||
return result.length > 0 ? result.trim() : value.slice(0, max).trim();
|
||||
}
|
||||
|
||||
// based on https://github.com/zuchka/remove-markdown
|
||||
export function removeMarkdown(value: string) {
|
||||
const htmlReplaceRegex = /<[^>]*>/g;
|
||||
return (
|
||||
value
|
||||
// Remove HTML tags
|
||||
.replace(htmlReplaceRegex, "")
|
||||
// Remove setext-style headers
|
||||
.replace(/^[=\-]{2,}\s*$/g, "")
|
||||
// Remove footnotes?
|
||||
.replace(/\[\^.+?\](\: .*?$)?/g, "")
|
||||
.replace(/\s{0,2}\[.*?\]: .*?$/g, "")
|
||||
// Remove images
|
||||
.replace(/\!\[(.*?)\][\[\(].*?[\]\)]/g, "")
|
||||
// Remove inline links
|
||||
.replace(/\[([^\]]*?)\][\[\(].*?[\]\)]/g, "$2")
|
||||
// Remove blockquotes
|
||||
.replace(/^(\n)?\s{0,3}>\s?/gm, "$1")
|
||||
// Remove reference-style links?
|
||||
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, "")
|
||||
// Remove headers
|
||||
.replaceAll("#", "")
|
||||
// Remove * emphasis
|
||||
.replace(/([\*]+)(\S)(.*?\S)??\1/g, "$2$3")
|
||||
// Remove _ emphasis. Unlike *, _ emphasis gets rendered only if
|
||||
// 1. Either there is a whitespace character before opening _ and after closing _.
|
||||
// 2. Or _ is at the start/end of the string.
|
||||
.replace(/(^|\W)([_]+)(\S)(.*?\S)??\2($|\W)/g, "$1$3$4$5")
|
||||
// Remove code blocks
|
||||
.replace(/(`{3,})(.*?)\1/gm, "$2")
|
||||
// Remove inline code
|
||||
.replace(/`(.+?)`/g, "$1")
|
||||
// // Replace two or more newlines with exactly two? Not entirely sure this belongs here...
|
||||
// .replace(/\n{2,}/g, '\n\n')
|
||||
// // Remove newlines in a paragraph
|
||||
// .replace(/(\S+)\n\s*(\S+)/g, '$1 $2')
|
||||
// Replace strike through
|
||||
.replace(/~(.*?)~/g, "$1")
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user