Bluesky via Discord connection + upgrade remix-auth + remove Twitter references (#2058)

* Remove Twitter references

* Upgrade remix auth, bsky via Discord

* Test
This commit is contained in:
Kalle 2025-01-28 17:22:45 +02:00 committed by GitHub
parent db10a177a8
commit 32c97a2467
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
85 changed files with 305 additions and 514 deletions

View File

@ -132,7 +132,7 @@ sendou.ink/
│ ├── components/ -- React components
│ ├── db/ -- Database layer
│ ├── hooks/ -- React hooks
│ ├── modules/ -- "nodu_modules but part of the app" https://twitter.com/ryanflorence/status/1535103735952658432
│ ├── modules/ -- "node_modules but part of the app"
│ ├── routes/ -- Routes see: https://remix.run/docs/en/v1/guides/routing
│ ├── styles/ -- All .css files of the project for styling
│ ├── utils/ -- Random helper functions used in many places

View File

@ -1,6 +1,8 @@
import React from "react";
import invariant from "~/utils/invariant";
// TODO: use react aria components
export function Dialog({
children,
isOpen,
@ -59,7 +61,6 @@ function useDOMSync(isOpen: boolean) {
if (isOpen) {
dialog.showModal();
// TODO: can be replaced with https://twitter.com/argyleink/status/1529869352660439048 once gets control
html.classList.add("lock-scroll");
} else {
dialog.close();

View File

@ -1,16 +0,0 @@
export function TwitterIcon({ className }: { className?: string }) {
return (
<svg
className={className}
stroke="currentColor"
fill="currentColor"
strokeWidth="0"
version="1.1"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<title>Twitter Icon</title>
<path d="M16 3.538c-0.588 0.263-1.222 0.438-1.884 0.516 0.678-0.406 1.197-1.050 1.444-1.816-0.634 0.375-1.338 0.65-2.084 0.797-0.6-0.638-1.453-1.034-2.397-1.034-1.813 0-3.281 1.469-3.281 3.281 0 0.256 0.028 0.506 0.084 0.747-2.728-0.138-5.147-1.444-6.766-3.431-0.281 0.484-0.444 1.050-0.444 1.65 0 1.138 0.578 2.144 1.459 2.731-0.538-0.016-1.044-0.166-1.488-0.409 0 0.013 0 0.028 0 0.041 0 1.591 1.131 2.919 2.634 3.219-0.275 0.075-0.566 0.116-0.866 0.116-0.212 0-0.416-0.022-0.619-0.059 0.419 1.303 1.631 2.253 3.066 2.281-1.125 0.881-2.538 1.406-4.078 1.406-0.266 0-0.525-0.016-0.784-0.047 1.456 0.934 3.181 1.475 5.034 1.475 6.037 0 9.341-5.003 9.341-9.341 0-0.144-0.003-0.284-0.009-0.425 0.641-0.459 1.197-1.038 1.637-1.697z" />
</svg>
);
}

View File

@ -14,7 +14,6 @@ export const USER = {
CUSTOM_NAME_MAX_LENGTH: 32,
CUSTOM_NAME_REGEXP: notAllEmptyCharactersRegExp,
BATTLEFY_MAX_LENGTH: 32,
BSKY_MAX_LENGTH: 50,
IN_GAME_NAME_TEXT_MAX_LENGTH: 20,
IN_GAME_NAME_DISCRIMINATOR_MAX_LENGTH: 5,
WEAPON_POOL_MAX_SIZE: 5,

View File

@ -245,7 +245,6 @@ async function adminUser() {
twitch: "Sendou",
youtubeId: "UCWbJLXByvsfQvTcR4HLPs5Q",
discordAvatar: ADMIN_TEST_AVATAR,
twitter: "sendouc",
discordUniqueName: "sendou",
});
}
@ -294,7 +293,6 @@ function nzapUser() {
twitch: null,
youtubeId: null,
discordAvatar: NZAP_TEST_AVATAR,
twitter: null,
discordUniqueName: null,
});
}
@ -474,7 +472,6 @@ function fakeUser(usedNames: Set<string>) {
discordId: String(faker.string.numeric(17)),
discordName: uniqueDiscordName(usedNames),
twitch: null,
twitter: null,
youtubeId: null,
discordUniqueName: null,
});
@ -1578,12 +1575,11 @@ function detailedTeam() {
sql
.prepare(
/* sql */ `
insert into "AllTeam" ("name", "customUrl", "inviteCode", "twitter", "bio", "avatarImgId", "bannerImgId")
insert into "AllTeam" ("name", "customUrl", "inviteCode", "bio", "avatarImgId", "bannerImgId")
values (
'Alliance Rogue',
'alliance-rogue',
'${nanoid(INVITE_CODE_LENGTH)}',
'AllianceRogueFR',
'${faker.lorem.paragraph()}',
1,
2
@ -1643,13 +1639,12 @@ function otherTeams() {
sql
.prepare(
/* sql */ `
insert into "AllTeam" ("id", "name", "customUrl", "inviteCode", "twitter", "bio")
insert into "AllTeam" ("id", "name", "customUrl", "inviteCode", "bio")
values (
@id,
@name,
@customUrl,
@inviteCode,
@twitter,
@bio
)
`,
@ -1659,7 +1654,6 @@ function otherTeams() {
name: teamName,
customUrl: teamCustomUrl,
inviteCode: nanoid(INVITE_CODE_LENGTH),
twitter: faker.internet.username(),
bio: faker.lorem.paragraph(),
});

View File

@ -34,7 +34,6 @@ export interface Team {
id: GeneratedAlways<number>;
inviteCode: string;
name: string;
twitter: string | null;
bsky: string | null;
}
@ -784,7 +783,6 @@ export interface User {
showDiscordUniqueName: Generated<number>;
stickSens: number | null;
twitch: string | null;
twitter: string | null;
bsky: string | null;
battlefy: string | null;
vc: Generated<"YES" | "NO" | "LISTEN_ONLY">;

View File

@ -23,7 +23,6 @@ export interface User {
discordUniqueName: string | null;
showDiscordUniqueName: number;
twitch: string | null;
twitter: string | null;
youtubeId: string | null;
bio: string | null;
css: string | null;
@ -419,7 +418,6 @@ export interface Team {
customUrl: string;
inviteCode: string;
css: string | null;
twitter: string | null;
bio: string | null;
avatarImgId: number | null;
bannerImgId: number | null;

View File

@ -32,7 +32,6 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
"User.country",
"User.discordName",
"User.twitch",
"User.twitter",
"User.battlefy",
"User.bsky",
"User.customUrl",
@ -95,9 +94,9 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
plusServerTier: user.tier as GetUserResponse["plusServerTier"],
socials: {
twitch: user.twitch,
twitter: user.twitter,
battlefy: user.battlefy,
bsky: user.bsky,
twitter: null, // deprecated field
},
peakXp:
user.xRankPlacements.length > 0

View File

@ -26,7 +26,8 @@ export interface GetUserResponse {
country: string | null;
socials: {
twitch: string | null;
twitter: string | null;
// @deprecated
twitter: null;
battlefy: string | null;
bsky: string | null;
};

View File

@ -53,7 +53,6 @@ export const meta: MetaFunction = (args) => {
{ property: "og:title", content: data.title },
{ name: "description", content: description },
{ property: "og:description", content: description },
{ name: "twitter:card", content: "summary_large_image" },
{ property: "og:image", content: articlePreviewUrl(args.params.slug) },
{ property: "og:type", content: "article" },
{ property: "og:site_name", content: "sendou.ink" },

View File

@ -1,15 +1,9 @@
import type { OAuth2Profile } from "remix-auth-oauth2";
import { OAuth2Strategy } from "remix-auth-oauth2";
import { z } from "zod";
import type { User } from "~/db/types";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
import { DISCORD_AUTH_KEY } from "./authenticator.server";
interface DiscordExtraParams extends Record<string, string | number> {
scope: string;
}
export type LoggedInUser = User["id"];
@ -35,88 +29,14 @@ const discordUserDetailsSchema = z.tuple([
partialDiscordConnectionsSchema,
]);
export class DiscordStrategy extends OAuth2Strategy<
LoggedInUser,
OAuth2Profile,
DiscordExtraParams
> {
name = DISCORD_AUTH_KEY;
scope: string;
export const DiscordStrategy = () => {
const envVars = authEnvVars();
constructor() {
const envVars = authEnvVars();
super(
{
authorizationURL: "https://discord.com/api/oauth2/authorize",
tokenURL:
process.env.AUTH_GATEWAY_TOKEN_URL ??
"https://discord.com/api/oauth2/token",
clientID: envVars.DISCORD_CLIENT_ID,
clientSecret: envVars.DISCORD_CLIENT_SECRET,
callbackURL: new URL("/auth/callback", envVars.BASE_URL).toString(),
},
async ({ accessToken }) => {
try {
const discordResponses = this.authGatewayEnabled()
? await this.fetchProfileViaGateway(accessToken)
: await this.fetchProfileViaDiscordApi(accessToken);
const [user, connections] =
discordUserDetailsSchema.parse(discordResponses);
const isAlreadyRegistered = Boolean(
await UserRepository.identifierToUserId(user.id),
);
if (!isAlreadyRegistered && !user.verified) {
logger.info(`User is not verified with id: ${user.id}`);
throw new Error("Unverified user");
}
const userFromDb = await UserRepository.upsert({
discordAvatar: user.avatar ?? null,
discordId: user.id,
discordName: user.global_name ?? user.username,
discordUniqueName: user.global_name ? user.username : null,
...this.parseConnections(connections),
});
return userFromDb.id;
} catch (e) {
console.error("Failed to finish authentication:\n", e);
throw new Error("Failed to finish authentication");
}
},
);
this.scope = "identify connections email";
}
private authGatewayEnabled() {
const authGatewayEnabled = () => {
return Boolean(process.env.AUTH_GATEWAY_TOKEN_URL);
}
};
private async fetchProfileViaDiscordApi(token: string) {
const authHeader: [string, string] = ["Authorization", `Bearer ${token}`];
return Promise.all([
fetch("https://discord.com/api/users/@me", {
headers: [authHeader],
}).then(this.jsonIfOk),
fetch("https://discord.com/api/users/@me/connections", {
headers: [authHeader],
}).then(this.jsonIfOk),
]);
}
private async fetchProfileViaGateway(token: string) {
const url = `${process.env.AUTH_GATEWAY_PROFILE_URL}?token=${token}`;
return fetch(url).then(this.jsonIfOk);
}
private jsonIfOk(res: Response) {
const jsonIfOk = (res: Response) => {
if (!res.ok) {
throw new Error(
`Auth related call failed with status code ${res.status}`,
@ -124,48 +44,106 @@ export class DiscordStrategy extends OAuth2Strategy<
}
return res.json();
}
};
private parseConnections(
connections: z.infer<typeof partialDiscordConnectionsSchema>,
) {
if (!connections) throw new Error("No connections");
const fetchProfileViaDiscordApi = (token: string) => {
const authHeader: [string, string] = ["Authorization", `Bearer ${token}`];
const result: {
twitch: string | null;
twitter: string | null;
youtubeId: string | null;
} = {
twitch: null,
twitter: null,
youtubeId: null,
};
return Promise.all([
fetch("https://discord.com/api/users/@me", {
headers: [authHeader],
}).then(jsonIfOk),
fetch("https://discord.com/api/users/@me/connections", {
headers: [authHeader],
}).then(jsonIfOk),
]);
};
for (const connection of connections) {
if (connection.visibility !== 1 || !connection.verified) continue;
const fetchProfileViaGateway = (token: string) => {
const url = `${process.env.AUTH_GATEWAY_PROFILE_URL}?token=${token}`;
switch (connection.type) {
case "twitch":
result.twitch = connection.name;
break;
case "twitter":
result.twitter = connection.name;
break;
case "youtube":
result.youtubeId = connection.id;
return fetch(url).then(jsonIfOk);
};
return new OAuth2Strategy(
{
clientId: envVars.DISCORD_CLIENT_ID,
clientSecret: envVars.DISCORD_CLIENT_SECRET,
authorizationEndpoint: "https://discord.com/api/oauth2/authorize",
tokenEndpoint:
process.env.AUTH_GATEWAY_TOKEN_URL ||
"https://discord.com/api/oauth2/token",
redirectURI: new URL("/auth/callback", envVars.BASE_URL).toString(),
scopes: ["identify", "connections", "email"],
},
async ({ tokens }) => {
try {
const discordResponses = authGatewayEnabled()
? await fetchProfileViaGateway(tokens.accessToken())
: await fetchProfileViaDiscordApi(tokens.accessToken());
const [user, connections] =
discordUserDetailsSchema.parse(discordResponses);
const isAlreadyRegistered = Boolean(
await UserRepository.identifierToUserId(user.id),
);
if (!isAlreadyRegistered && !user.verified) {
logger.info(`User is not verified with id: ${user.id}`);
throw new Error("Unverified user");
}
const userFromDb = await UserRepository.upsert({
discordAvatar: user.avatar ?? null,
discordId: user.id,
discordName: user.global_name ?? user.username,
discordUniqueName: user.global_name ? user.username : null,
...parseConnections(connections),
});
return userFromDb.id;
} catch (e) {
console.error("Failed to finish authentication:\n", e);
throw new Error("Failed to finish authentication");
}
},
);
};
function parseConnections(
connections: z.infer<typeof partialDiscordConnectionsSchema>,
) {
if (!connections) throw new Error("No connections");
const result: {
twitch: string | null;
youtubeId: string | null;
bsky: string | null;
} = {
twitch: null,
youtubeId: null,
bsky: null,
};
for (const connection of connections) {
if (connection.visibility !== 1 || !connection.verified) continue;
switch (connection.type) {
case "twitch":
result.twitch = connection.name;
break;
case "youtube":
result.youtubeId = connection.id;
break;
case "bluesky":
result.bsky = connection.name;
}
return result;
}
protected authorizationParams() {
const urlSearchParams: Record<string, string> = {
scope: this.scope,
};
return new URLSearchParams(urlSearchParams);
}
return result;
}
function authEnvVars() {

View File

@ -1,17 +1,9 @@
import { Authenticator } from "remix-auth";
import { DiscordStrategy } from "./DiscordStrategy.server";
import type { LoggedInUser } from "./DiscordStrategy.server";
import { authSessionStorage } from "./session.server";
import { DiscordStrategy, type LoggedInUser } from "./DiscordStrategy.server";
export const DISCORD_AUTH_KEY = "discord";
export const SESSION_KEY = "user";
export const IMPERSONATED_SESSION_KEY = "impersonated_user";
export const authenticator = new Authenticator<LoggedInUser>(
authSessionStorage,
{
sessionKey: SESSION_KEY,
},
);
export const authenticator = new Authenticator<LoggedInUser>();
authenticator.use(new DiscordStrategy());
authenticator.use(DiscordStrategy(), "discord");

View File

@ -4,13 +4,13 @@ import { isbot } from "isbot";
import { z } from "zod";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { canAccessLohiEndpoint, canPerformAdminActions } from "~/permissions";
import { logger } from "~/utils/logger";
import { parseSearchParams, validate } from "~/utils/remix.server";
import { ADMIN_PAGE, authErrorUrl } from "~/utils/urls";
import { createLogInLink } from "../queries/createLogInLink.server";
import { deleteLogInLinkByCode } from "../queries/deleteLogInLinkByCode.server";
import { userIdByLogInLinkCode } from "../queries/userIdByLogInLinkCode.server";
import {
DISCORD_AUTH_KEY,
IMPERSONATED_SESSION_KEY,
SESSION_KEY,
authenticator,
@ -22,23 +22,42 @@ export const callbackLoader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url);
if (url.searchParams.get("error") === "access_denied") {
// The user denied the authentication request
// This is part of the oauth2 protocol, but remix-auth-oauth2 doesn't do
// nice error handling for this case.
// https://www.oauth.com/oauth2-servers/server-side-apps/possible-errors/
throw redirect(authErrorUrl("aborted"));
}
await authenticator.authenticate(DISCORD_AUTH_KEY, request, {
successRedirect: "/",
failureRedirect: authErrorUrl("unknown"),
});
try {
const userId = await authenticator.authenticate("discord", request);
throw new Response("Unknown authentication state", { status: 500 });
const session = await authSessionStorage.getSession(
request.headers.get(SESSION_KEY),
);
session.set(SESSION_KEY, userId);
return redirect("/", {
headers: {
"Set-Cookie": await authSessionStorage.commitSession(session),
},
});
} catch (error) {
if (error instanceof Error) {
logger.error("Error during authentication:", error);
throw redirect(authErrorUrl("unknown"));
}
throw error;
}
};
export const logOutAction: ActionFunction = async ({ request }) => {
await authenticator.logout(request, { redirectTo: "/" });
const session = await authSessionStorage.getSession(
request.headers.get(SESSION_KEY),
);
return redirect("/", {
headers: { "Set-Cookie": await authSessionStorage.destroySession(session) },
});
};
export const logInAction: ActionFunction = async ({ request }) => {
@ -47,7 +66,7 @@ export const logInAction: ActionFunction = async ({ request }) => {
"Login is temporarily disabled",
);
return await authenticator.authenticate(DISCORD_AUTH_KEY, request);
return await authenticator.authenticate("discord", request);
};
export const impersonateAction: ActionFunction = async ({ request }) => {

View File

@ -8,15 +8,9 @@ import { languages } from "~/modules/i18n/config";
import type { SendouRouteHandle } from "~/utils/remix.server";
import { makeTitle } from "~/utils/strings";
import {
ANTARISKA_TWITTER,
BORZOIC_TWITTER,
GITHUB_CONTRIBUTORS_URL,
LEAN_TWITTER,
RHODESMAS_FREESOUND_PROFILE_URL,
SENDOU_TWITTER_URL,
SPLATOON_3_INK,
UBERU_TWITTER,
YAGA_TWITTER,
} from "~/utils/urls";
export const meta: MetaFunction = () => {
@ -38,7 +32,7 @@ const PROGRAMMERS = [
] as const;
const TRANSLATORS: Array<{
translators: Array<string | { name: string; twitter: string }>;
translators: Array<string>;
language: (typeof languages)[number]["code"];
}> = [
{
@ -46,11 +40,7 @@ const TRANSLATORS: Array<{
language: "da",
},
{
translators: [
{ name: "NoAim™bUrn", twitter: "noaim_brn" },
{ name: "Alice", twitter: "Aloschus" },
"jgiefer",
],
translators: ["NoAim™bUrn", "Alice", "jgiefer"],
language: "de",
},
{
@ -74,7 +64,7 @@ const TRANSLATORS: Array<{
language: "he",
},
{
translators: [{ name: "funyaaa", twitter: "funyaaa1" }, "taqm", "yutarour"],
translators: ["funyaaa", "taqm", "yutarour"],
language: "ja",
},
{
@ -86,15 +76,15 @@ const TRANSLATORS: Array<{
language: "pl",
},
{
translators: [{ name: "Ant", twitter: "Ant__Spl" }],
translators: ["Ant"],
language: "pt-BR",
},
{
translators: [{ name: "Ferrari", twitter: "Blusling" }],
translators: ["Ferrari"],
language: "nl",
},
{
translators: [{ name: "DoubleCookies", twitter: "DblCookies" }, "yaga"],
translators: ["DoubleCookies", "yaga"],
language: "ru",
},
{
@ -111,11 +101,7 @@ export default function ContributionsPage() {
<Main>
<p>
<Trans i18nKey={"contributions:project"} t={t}>
Sendou.ink is a project by{" "}
<a href={SENDOU_TWITTER_URL} target="_blank" rel="noreferrer">
Sendou
</a>{" "}
with help from contributors:
Sendou.ink is a project by Sendou with help from contributors:
</Trans>
</p>
<ul className="mt-2">
@ -125,36 +111,11 @@ export default function ContributionsPage() {
{t("contributions:code")}
</a>
</li>
<li>
<a href={LEAN_TWITTER} target="_blank" rel="noreferrer">
Lean
</a>{" "}
- {t("contributions:lean")}
</li>
<li>
<a href={BORZOIC_TWITTER} target="_blank" rel="noreferrer">
borzoic
</a>{" "}
- {t("contributions:borzoic")}
</li>
<li>
<a href={UBERU_TWITTER} target="_blank" rel="noreferrer">
uberu
</a>{" "}
- {t("contributions:uberu")}
</li>
<li>
<a href={YAGA_TWITTER} target="_blank" rel="noreferrer">
yaga
</a>{" "}
- {t("contributions:yaga")}
</li>
<li>
<a href={ANTARISKA_TWITTER} target="_blank" rel="noreferrer">
Antariska, yaga & harryXYZ
</a>{" "}
- {t("contributions:antariska")}
</li>
<li>Lean - {t("contributions:lean")}</li>
<li>borzoic - {t("contributions:borzoic")}</li>
<li>uberu - {t("contributions:uberu")}</li>
<li>yaga - {t("contributions:yaga")}</li>
<li>Antariska, yaga & harryXYZ - {t("contributions:antariska")}</li>
<li>
<a href={SPLATOON_3_INK} target="_blank" rel="noreferrer">
splatoon3.ink
@ -163,27 +124,12 @@ export default function ContributionsPage() {
</li>
{TRANSLATORS.map(({ translators, language }) => (
<li key={language}>
{translators
.map((t) =>
typeof t === "string" ? (
t
) : (
<a
key={t.name}
href={`https://twitter.com/${t.twitter}`}
rel="noreferrer"
target="_blank"
>
{t.name}
</a>
),
)
.map((element, i, arr) => (
<React.Fragment key={i}>
{element}
{i !== arr.length - 1 ? ", " : null}
</React.Fragment>
))}{" "}
{translators.map((element, i, arr) => (
<React.Fragment key={i}>
{element}
{i !== arr.length - 1 ? ", " : null}
</React.Fragment>
))}{" "}
- {t("contributions:translation")} (
{languages.find((lang) => lang.code === language)!.name})
</li>

View File

@ -335,7 +335,6 @@ function SuggestedUser({
suggested: { id: suggestion.suggested.id },
targetPlusTier: Number(tier),
}) ? (
// TODO: resetScroll={false} https://twitter.com/ryanflorence/status/1527775882797907969
<LinkButton
className="plus__comment-button"
size="tiny"

View File

@ -392,15 +392,7 @@ function OtherTopics() {
less points than they do. This is possible because of confidence
rating which is an internal value that goes down when you perform as
the algorithm expects you to perform and goes up when it&apos;s the
opposite. Check Joy&apos;s{" "}
<a
target="_blank"
rel="noreferrer"
href="https://twitter.com/JoyTheDataNerd/status/1709651029971570960"
>
Twitter thread
</a>{" "}
on a longer explanation.
opposite.
</p>
<p>
Only the whole set&apos;s result matters so points gained/lost are the

View File

@ -81,7 +81,7 @@ export default function SendouqRules() {
<h2 className="text-lg mt-4">Player eligibility</h2>
<div>
Players banned by{" "}
<a href="https://twitter.com/splatsafety">
<a href="https://bsky.app/profile/splatsafety.bsky.social">
Splatoon Competitive Community Safety
</a>{" "}
are not allowed to participate. Playing with banned players is not

View File

@ -67,7 +67,6 @@ export function findByCustomUrl(customUrl: string) {
.select(({ eb }) => [
"Team.id",
"Team.name",
"Team.twitter",
"Team.bsky",
"Team.bio",
"Team.customUrl",
@ -148,12 +147,11 @@ export async function update({
name,
customUrl,
bio,
twitter,
bsky,
css,
}: Pick<
Insertable<Tables["Team"]>,
"id" | "name" | "customUrl" | "bio" | "twitter" | "bsky"
"id" | "name" | "customUrl" | "bio" | "bsky"
> & { css: string | null }) {
return db
.updateTable("AllTeam")
@ -161,7 +159,6 @@ export async function update({
name,
customUrl,
bio,
twitter,
bsky,
css,
})

View File

@ -31,7 +31,6 @@ describe("team creation", () => {
bio: null,
bsky: null,
css: null,
twitter: null,
},
{ user: "regular", params: { customUrl: "team-1" } },
);
@ -50,7 +49,6 @@ describe("team creation", () => {
bio: null,
bsky: null,
css: null,
twitter: null,
},
{ user: "regular", params: { customUrl: "team-1" } },
),

View File

@ -25,7 +25,7 @@ import {
parseRequestPayload,
validate,
} from "~/utils/remix.server";
import { makeTitle, pathnameFromPotentialURL } from "~/utils/strings";
import { makeTitle } from "~/utils/strings";
import { assertUnreachable } from "~/utils/types";
import {
TEAM_SEARCH_PAGE,
@ -158,7 +158,6 @@ export default function EditTeamPage() {
<CustomizedColorsInput initialColors={css} />
) : null}
<NameInput />
<TwitterInput />
<BlueskyInput />
<BioTextarea />
<SubmitButton
@ -230,27 +229,6 @@ function NameInput() {
);
}
function TwitterInput() {
const { t } = useTranslation(["team"]);
const { team } = useLoaderData<typeof loader>();
const [value, setValue] = React.useState(team.twitter ?? "");
return (
<div>
<Label htmlFor="twitter">{t("team:forms.fields.teamTwitter")}</Label>
<Input
leftAddon="https://twitter.com/"
id="twitter"
name="twitter"
maxLength={TEAM.TWITTER_MAX_LENGTH}
value={value}
onChange={(e) => setValue(pathnameFromPotentialURL(e.target.value))}
testId="twitter-input"
/>
</div>
);
}
function BlueskyInput() {
const { t } = useTranslation(["team"]);
const { team } = useLoaderData<typeof loader>();

View File

@ -13,7 +13,6 @@ import { SubmitButton } from "~/components/SubmitButton";
import { BskyIcon } from "~/components/icons/Bsky";
import { EditIcon } from "~/components/icons/Edit";
import { StarIcon } from "~/components/icons/Star";
import { TwitterIcon } from "~/components/icons/Twitter";
import { UsersIcon } from "~/components/icons/Users";
import { useUser } from "~/features/auth/core/user";
import { isAdmin } from "~/permissions";
@ -27,7 +26,6 @@ import {
manageTeamRosterPage,
navIconUrl,
teamPage,
twitterUrl,
userPage,
userSubmittedImage,
} from "~/utils/urls";
@ -128,7 +126,7 @@ function TeamBanner() {
})}
</div>
<div className="team__banner__name">
{team.name} <TwitterLink testId="twitter-link" /> <BskyLink />
{team.name} <BskyLink />
</div>
</div>
{team.avatarSrc ? <div className="team__banner__avatar__spacer" /> : null}
@ -152,31 +150,12 @@ function MobileTeamNameCountry() {
</div>
<div className="team__mobile-team-name">
{team.name}
<TwitterLink />
<BskyLink />
</div>
</div>
);
}
function TwitterLink({ testId }: { testId?: string }) {
const { team } = useLoaderData<typeof loader>();
if (!team.twitter) return null;
return (
<a
className="team__twitter-link"
href={twitterUrl(team.twitter)}
target="_blank"
rel="noreferrer"
data-testid={testId}
>
<TwitterIcon />
</a>
);
}
function BskyLink() {
const { team } = useLoaderData<typeof loader>();
@ -185,6 +164,7 @@ function BskyLink() {
return (
<a
className="team__bsky-link"
data-testid="bsky-link"
href={bskyUrl(team.bsky)}
target="_blank"
rel="noreferrer"

View File

@ -2,7 +2,6 @@ export const TEAM = {
NAME_MAX_LENGTH: 64,
NAME_MIN_LENGTH: 2,
BIO_MAX_LENGTH: 2000,
TWITTER_MAX_LENGTH: 50,
BSKY_MAX_LENGTH: 50,
MAX_MEMBER_COUNT: 10,
MAX_TEAM_COUNT_NON_PATRON: 2,

View File

@ -28,10 +28,6 @@ export const editTeamSchema = z.union([
falsyToNull,
z.string().max(TEAM.BIO_MAX_LENGTH).nullable(),
),
twitter: z.preprocess(
falsyToNull,
z.string().max(TEAM.TWITTER_MAX_LENGTH).nullable(),
),
bsky: z.preprocess(
falsyToNull,
z.string().max(TEAM.BSKY_MAX_LENGTH).nullable(),

View File

@ -84,19 +84,6 @@
gap: var(--s-3);
}
.team__twitter-link {
padding: var(--s-1);
border: 1px solid;
border-radius: 50%;
border-color: #1da1f2;
background-color: #1da0f22f;
}
.team__twitter-link > svg {
width: 0.9rem;
fill: #1da1f2;
}
.team__bsky-link {
padding: var(--s-1);
border: 1px solid;

View File

@ -2,7 +2,6 @@ import clsx from "clsx";
import { BskyIcon } from "~/components/icons/Bsky";
import { LinkIcon } from "~/components/icons/Link";
import { TwitchIcon } from "~/components/icons/Twitch";
import { TwitterIcon } from "~/components/icons/Twitter";
import { YouTubeIcon } from "~/components/icons/YouTube";
export function SocialLinksList({ links }: { links: string[] }) {
@ -23,7 +22,6 @@ function SocialLink({ url }: { url: string }) {
<div
className={clsx("org__social-link__icon-container", {
youtube: type === "youtube",
twitter: type === "twitter",
twitch: type === "twitch",
bsky: type === "bsky",
})}
@ -38,10 +36,6 @@ function SocialLink({ url }: { url: string }) {
function SocialLinkIcon({ url }: { url: string }) {
const type = urlToLinkType(url);
if (type === "twitter") {
return <TwitterIcon />;
}
if (type === "twitch") {
return <TwitchIcon />;
}
@ -58,10 +52,6 @@ function SocialLinkIcon({ url }: { url: string }) {
}
const urlToLinkType = (url: string) => {
if (url.includes("twitter.com") || url.includes("x.com")) {
return "twitter";
}
if (url.includes("twitch.tv")) {
return "twitch";
}

View File

@ -61,14 +61,6 @@ export const meta: MetaFunction = (args) => {
? userSubmittedImage(data.organization.avatarUrl)
: undefined,
},
{
name: "twitter:card",
content: "summary",
},
{
name: "twitter:title",
content: title,
},
];
};

View File

@ -178,10 +178,6 @@
fill: #f00;
}
.org__social-link__icon-container.twitter svg {
fill: #1da1f2;
}
.org__social-link__icon-container.bsky path {
fill: #1285fe;
}

View File

@ -87,19 +87,6 @@ export const meta: MetaFunction = (args) => {
property: "og:image",
content: ogImage(),
},
// Twitter special snowflake tags, see https://developer.x.com/en/docs/twitter-for-websites/cards/overview/summary
{
name: "twitter:card",
content: "summary",
},
{
name: "twitter:title",
content: title,
},
{
name: "twitter:site",
content: "@sendouink",
},
];
};

View File

@ -137,7 +137,6 @@ export async function findProfileByIdentifier(
.leftJoin("PlusTier", "PlusTier.userId", "User.id")
.select(({ eb }) => [
"User.twitch",
"User.twitter",
"User.youtubeId",
"User.battlefy",
"User.bsky",
@ -461,7 +460,6 @@ export async function search({
eb("User.username", "like", query),
eb("User.inGameName", "like", query),
eb("User.discordUniqueName", "like", query),
eb("User.twitter", "like", query),
eb("User.customUrl", "like", query),
]),
)
@ -490,7 +488,6 @@ export async function search({
eb("User.username", "like", fuzzyQuery),
eb("User.inGameName", "like", fuzzyQuery),
eb("User.discordUniqueName", "like", fuzzyQuery),
eb("User.twitter", "like", fuzzyQuery),
])
.and(
"User.id",
@ -575,8 +572,8 @@ export function upsert(
| "discordAvatar"
| "discordUniqueName"
| "twitch"
| "twitter"
| "youtubeId"
| "bsky"
>,
) {
return db
@ -601,7 +598,6 @@ type UpdateProfileArgs = Pick<
| "stickSens"
| "inGameName"
| "battlefy"
| "bsky"
| "css"
| "favoriteBadgeId"
| "showDiscordUniqueName"
@ -644,7 +640,6 @@ export function updateProfile(args: UpdateProfileArgs) {
inGameName: args.inGameName,
css: args.css,
battlefy: args.battlefy,
bsky: args.bsky,
favoriteBadgeId: args.favoriteBadgeId,
showDiscordUniqueName: args.showDiscordUniqueName,
commissionText: args.commissionText,

View File

@ -99,10 +99,6 @@ const userEditActionSchema = z
falsyToNull,
z.string().max(USER.BATTLEFY_MAX_LENGTH).nullable(),
),
bsky: z.preprocess(
falsyToNull,
z.string().max(USER.BSKY_MAX_LENGTH).nullable(),
),
stickSens: z.preprocess(
processMany(actualNumber, undefinedToNull),
z
@ -269,7 +265,6 @@ export default function UserEditPage() {
<InGameNameInputs />
<SensSelects />
<BattlefyInput />
<BskyInput />
<CountrySelect />
<FavBadgeSelect />
<WeaponPoolSelect />
@ -292,7 +287,7 @@ export default function UserEditPage() {
)}
<FormMessage type="info">
<Trans i18nKey={"user:discordExplanation"} t={t}>
Username, profile picture, YouTube, Twitter and Twitch accounts come
Username, profile picture, YouTube, Bluesky and Twitch accounts come
from your Discord account. See <Link to={FAQ_PAGE}>FAQ</Link> for
more information.
</Trans>
@ -469,24 +464,6 @@ function BattlefyInput() {
);
}
function BskyInput() {
const { t } = useTranslation(["user"]);
const data = useLoaderData<typeof loader>();
return (
<div className="w-full">
<Label htmlFor="bsky">{t("user:bsky")}</Label>
<Input
name="bsky"
id="bsky"
maxLength={USER.BSKY_MAX_LENGTH}
defaultValue={data.user.bsky ?? undefined}
leftAddon="https://bsky.app/profile/"
/>
</div>
);
}
function WeaponPoolSelect() {
const data = useLoaderData<typeof loader>();
const [weapons, setWeapons] = React.useState(data.user.weapons);

View File

@ -9,7 +9,6 @@ import { BattlefyIcon } from "~/components/icons/Battlefy";
import { BskyIcon } from "~/components/icons/Bsky";
import { DiscordIcon } from "~/components/icons/Discord";
import { TwitchIcon } from "~/components/icons/Twitch";
import { TwitterIcon } from "~/components/icons/Twitter";
import { YouTubeIcon } from "~/components/icons/YouTube";
import { BadgeDisplay } from "~/features/badges/components/BadgeDisplay";
import { modesShort } from "~/modules/in-game-lists";
@ -60,9 +59,6 @@ export default function UserInfoPage() {
{data.user.twitch ? (
<SocialLink type="twitch" identifier={data.user.twitch} />
) : null}
{data.user.twitter ? (
<SocialLink type="twitter" identifier={data.user.twitter} />
) : null}
{data.user.youtubeId ? (
<SocialLink type="youtube" identifier={data.user.youtubeId} />
) : null}
@ -173,7 +169,7 @@ function SecondaryTeamsPopover() {
}
interface SocialLinkProps {
type: "youtube" | "twitter" | "twitch" | "battlefy" | "bsky";
type: "youtube" | "twitch" | "battlefy" | "bsky";
identifier: string;
}
@ -188,8 +184,6 @@ export function SocialLink({
switch (type) {
case "twitch":
return `https://www.twitch.tv/${identifier}`;
case "twitter":
return `https://www.twitter.com/${identifier}`;
case "youtube":
return `https://www.youtube.com/channel/${identifier}`;
case "battlefy":
@ -205,7 +199,6 @@ export function SocialLink({
<a
className={clsx("u__social-link", {
youtube: type === "youtube",
twitter: type === "twitter",
twitch: type === "twitch",
battlefy: type === "battlefy",
bsky: type === "bsky",
@ -221,8 +214,6 @@ function SocialLinkIcon({ type }: Pick<SocialLinkProps, "type">) {
switch (type) {
case "twitch":
return <TwitchIcon />;
case "twitter":
return <TwitterIcon />;
case "youtube":
return <YouTubeIcon />;
case "battlefy":

View File

@ -70,15 +70,6 @@
fill: #f00;
}
.u__social-link.twitter {
border-color: #1da1f2;
background-color: #1da0f22f;
}
.u__social-link.twitter > svg {
fill: #1da1f2;
}
.u__social-link.twitch {
border-color: #9146ff;
background-color: #9146ff2f;

View File

@ -3,7 +3,7 @@ import { pathnameFromPotentialURL } from "./strings";
describe("pathnameFromPotentialURL()", () => {
test("Resolves path name from valid URL", () => {
expect(pathnameFromPotentialURL("https://twitter.com/sendouc")).toBe(
expect(pathnameFromPotentialURL("https://bsky.app/sendouc")).toBe(
"sendouc",
);
});

View File

@ -67,7 +67,6 @@ export const conditionalUserSubmittedImage = (fileName: string) =>
export const PLUS_SERVER_DISCORD_URL = "https://discord.gg/FW4dKrY";
export const SENDOU_INK_DISCORD_URL = "https://discord.gg/sendou";
export const SENDOU_TWITTER_URL = "https://twitter.com/sendouc";
export const SENDOU_INK_PATREON_URL = "https://patreon.com/sendou";
export const NINTENDO_COMMUNITY_TOURNAMENTS_GUIDELINES_URL =
"https://en-americas-support.nintendo.com/app/answers/detail/a_id/63454";
@ -76,11 +75,6 @@ export const PATREON_HOW_TO_CONNECT_DISCORD_URL =
export const SENDOU_INK_GITHUB_URL = "https://github.com/Sendouc/sendou.ink";
export const GITHUB_CONTRIBUTORS_URL =
"https://github.com/Sendouc/sendou.ink/graphs/contributors";
export const BORZOIC_TWITTER = "https://twitter.com/borzoic_";
export const LEAN_TWITTER = "https://twitter.com/LeanYoshi";
export const UBERU_TWITTER = "https://twitter.com/uberu5";
export const YAGA_TWITTER = "https://twitter.com/a_bog_hag";
export const ANTARISKA_TWITTER = "https://twitter.com/antariska_spl";
export const ipLabsMaps = (pool: string) =>
`https://maps.iplabs.ink/?3&pool=${pool}`;
export const SPLATOON_3_INK = "https://splatoon3.ink/";
@ -89,8 +83,6 @@ export const RHODESMAS_FREESOUND_PROFILE_URL =
export const SPR_INFO_URL =
"https://www.pgstats.com/articles/introducing-spr-and-uf";
export const twitterUrl = (accountName: string) =>
`https://twitter.com/${accountName}`;
export const bskyUrl = (accountName: string) =>
`https://bsky.app/profile/${accountName}`;
export const twitchUrl = (accountName: string) =>

View File

@ -58,8 +58,8 @@ test.describe("Team page", () => {
await page.getByTestId("name-input").clear();
await page.getByTestId("name-input").fill("Better Alliance Rogue");
await page.getByTestId("twitter-input").clear();
await page.getByTestId("twitter-input").fill("BetterAllianceRogue");
await page.getByLabel("Team Bluesky").clear();
await page.getByLabel("Team Bluesky").fill("BetterAllianceRogue");
await page.getByTestId("bio-textarea").clear();
await page.getByTestId("bio-textarea").fill("shorter bio");
@ -68,9 +68,9 @@ test.describe("Team page", () => {
await expect(page).toHaveURL(/better-alliance-rogue/);
await page.getByText("shorter bio").isVisible();
await expect(page.getByTestId("twitter-link")).toHaveAttribute(
await expect(page.getByTestId("bsky-link").first()).toHaveAttribute(
"href",
"https://twitter.com/BetterAllianceRogue",
"https://bsky.app/profile/BetterAllianceRogue",
);
});

View File

@ -13,7 +13,7 @@
"q5": "Findes der en sendou.ink app?",
"a5": "Nej, men man kan installere Sendou ink som en ”Progressive Web App”, som fungerer som en traditionel app. Hvor man understøttelse af fuld skærm, skrivebordsikon og kører som en proces separat fra en webbrowser. Google ”How to install pwa on *din browser for at sfinde en vejledning til at gøre dette.",
"q6": "Hvordan tilføjer man en Twitch/Twitter/YouTube-profil til min sendou.ink-profil?",
"q6": "Hvordan tilføjer man en Twitch/Bluesky/YouTube-profil til min sendou.ink-profil?",
"a6": "Vi bruger Discord til dette formål. Tilknyt profilerne til Discord og verificér dem. Log derefter af og på sendou.ink for at få tilknyttet dine profiler på sendou.ink. ",
"q7": "Hvordan kan jeg uploade videoer?",

View File

@ -21,7 +21,6 @@
"roles.FLEX": "Fleksibel",
"roles.SUB": "Vikar",
"roles.COACH": "Træner",
"forms.fields.teamTwitter": "Holdets Twitter",
"forms.fields.bio": "Bio",
"forms.fields.uploadImages": "Upload billeder",
"forms.fields.uploadImages.pfp": "Profilbillede",

View File

@ -11,7 +11,7 @@
"stick": "Styrepind",
"sens": "Følsomhed",
"weaponPool": "Våbenpulje",
"discordExplanation": "Brugernavn, Profilbillede, Youtube-, Twitter- og Twitch-konter er hentet via din Discord-konto. Se <1>FAQ</1> for yderligere information.",
"discordExplanation": "Brugernavn, Profilbillede, Youtube-, Bluesky- og Twitch-konter er hentet via din Discord-konto. Se <1>FAQ</1> for yderligere information.",
"favoriteBadge": "Yndlingsmærke",
"battlefy": "Battlefy brugernavn",
@ -42,7 +42,6 @@
"forms.info.favoriteBadge": "Dit yndlingsmærke bliver som standard vist i stor størrelse på din profil",
"forms.info.battlefy": "Battlefy-brugernavn bruges til seeding og bekræftelse i nogle turneringer",
"search.info": "Søg efter brugere ved hjælp af deres Discord-, Splatoon 3- eller Twitter-navn",
"search.noResults": "Søgningen {{query}} fandt ingen brugere",
"seasons": "Sæsoner",

View File

@ -11,6 +11,6 @@
"q5": "Gibt es eine sendou.ink App?",
"a5": "Nein, aber sendou.ink kann als 'Progressive Web App' installiert werden. Dies bietet viele der Vorteile einer App, wie Vollbildmodus, Icon auf dem Homebildschirm oder separater Browser-Prozess. Google nach 'wie pwa auf *deinem browser* installieren' für eine Anleitung.",
"q6": "Wie füge ich Twitch/Twitter/YouTube zu meinem Profil hinzu?",
"q6": "Wie füge ich Twitch/Bluesky/YouTube zu meinem Profil hinzu?",
"a6": "Dafür wird dein Discord-Profil verwendet. Verknüpfe und verifiziere deine Profile auf Discord und logge dich dann auf sendou.ink aus und wieder ein, um sie zu aktualisieren."
}

View File

@ -19,7 +19,6 @@
"roles.MIDLINE": "Midline",
"roles.BACKLINE": "Backline",
"roles.COACH": "Coach",
"forms.fields.teamTwitter": "Team-Twitter",
"forms.fields.bio": "Bio",
"forms.fields.uploadImages": "Bilder hochladen",
"forms.fields.uploadImages.pfp": "Profilbild",

View File

@ -10,7 +10,7 @@
"stick": "Stick",
"sens": "Empfindlichkeit",
"weaponPool": "Waffenpool",
"discordExplanation": "Der Username, Profilbild, YouTube-, Twitter- und Twitch-Konten stammen von deinem Discord-Konto. Mehr Infos in den <1>FAQ</1>.",
"discordExplanation": "Der Username, Profilbild, YouTube-, Bluesky- und Twitch-Konten stammen von deinem Discord-Konto. Mehr Infos in den <1>FAQ</1>.",
"results.placing": "Platzierung",
"results.team": "Team",
@ -27,6 +27,5 @@
"forms.errors.invalidCustomUrl.duplicate": "Diese Benutzerdefinierte URL wird bereits verwendet",
"forms.errors.invalidSens": "Empfindlichkeit der Bewegungssteuerung kann nur festgelegt werden, wenn Empfindlichkeit R-Stick festgelegt ist",
"search.info": "Suche nach Nutzern anhand des Discord-, Splatoon-3- oder Twitter-Namens",
"search.noResults": "Keine Nutzer gefunden, die '{{query}}' entsprechen"
}

View File

@ -44,7 +44,7 @@
"auth.errors.aborted": "Login Aborted",
"auth.errors.failed": "Login Failed",
"auth.errors.discordPermissions": "For your sendou.ink profile, the site needs access to your Discord profile's name, avatar and social connections.",
"auth.errors.unknown": "Does your Discord account have a verified email? For help please reach out to us via the #helpdesk channel on our Discord:",
"auth.errors.unknown": "Unknown error, try again a bit later. Verify also that your Discord account has a verified email. For help please reach out to us via the #helpdesk channel on our Discord:",
"footer.github.subtitle": "Source code",
"footer.discord.subtitle": "Help & feedback",

View File

@ -14,7 +14,7 @@
"q5": "Is there a sendou.ink app?",
"a5": "No, but sendou.ink can be installed as a 'Progressive Web App' that gets many of the same benefits such as full-screen, an icon on the home screen and a separate process from the browser. Google 'how to install PWA on *your browser*' to see how.",
"q6": "How to add Twitch/Twitter/YouTube to my profile?",
"q6": "How to add Twitch/Bluesky/YouTube to my profile?",
"a6": "We use your Discord profile for that. Connect and verify them on Discord then log out and back in on sendou.ink to have them update.",
"q7": "How can I add videos?",

View File

@ -25,7 +25,6 @@
"roles.SUB": "Sub",
"roles.COACH": "Coach",
"roles.CHEERLEADER": "Cheerleader",
"forms.fields.teamTwitter": "Team Twitter",
"forms.fields.teamBsky": "Team Bluesky",
"forms.fields.bio": "Bio",
"forms.fields.uploadImages": "Upload images",

View File

@ -11,10 +11,9 @@
"stick": "Stick",
"sens": "Sens",
"weaponPool": "Weapon pool",
"discordExplanation": "Username, profile picture, YouTube, Twitter and Twitch accounts come from your Discord account. See <1>FAQ</1> for more information.",
"discordExplanation": "Username, profile picture, YouTube, Bluesky and Twitch accounts come from your Discord account. See <1>FAQ</1> for more information.",
"favoriteBadge": "Favorite Badge",
"battlefy": "Battlefy account name",
"bsky": "Bluesky account name",
"forms.showDiscordUniqueName": "Show Discord username",
"forms.showDiscordUniqueName.info": "Show your unique Discord name ({{discordUniqueName}}) publicly?",
@ -44,7 +43,7 @@
"forms.info.favoriteBadge": "Your favorite badge is shown as big by default on your profile.",
"forms.info.battlefy": "Battlefy account name is used for seeding and verification in some tournaments",
"search.info": "Search for users by Discord, Splatoon 3 or Twitter name",
"search.info": "Search for users by Discord or Splatoon 3 name",
"search.noResults": "No users found matching '{{query}}'",
"seasons": "Seasons",

View File

@ -33,7 +33,6 @@
"auth.errors.aborted": "Ingreso cancelado",
"auth.errors.failed": "Ingreso fallido",
"auth.errors.discordPermissions": "Para tu perfil en sendou.ink, el sitio requiere aceso a tu nombre en Discord, avatar, y redes sociales.",
"auth.errors.unknown": "Ingreso fallido debido a un error de servidor. Por favor intenta más tarde o usa /login a travez de Lohi bot. Se puede encontrar en el Discord de sendou.ink:",
"footer.github.subtitle": "Código fuente",
"footer.discord.subtitle": "Ayuda y comentarios",

View File

@ -13,7 +13,7 @@
"q5": "¿Hay un app de sendou.ink?",
"a5": "No, pero sendou.ink se puede instalar como una 'aplicación web progresiva' (PWA) que obtiene muchos de los mismos beneficios, como pantalla completa, un ícono en la pantalla de inicio y un proceso separado. Busque en Google 'cómo instalar PWA en *su navegador*' para encontrar instrucciones.",
"q6": "¿Cómo puedo agregar Twitch/Twitter/YouTube a mi perfil?",
"q6": "¿Cómo puedo agregar Twitch/Bluesky/YouTube a mi perfil?",
"a6": "Usamos tu perfil de Discord para eso. Conéctalos y verifícalos a travéz de Discord. Luego, cierra tu sesión y vuelve a iniciar tu sesión en sendou.ink para actualizar.",
"q7": "¿Cómo puedo agregar videos?",

View File

@ -20,7 +20,6 @@
"roles.BACKLINE": "Defensor",
"roles.FLEX": "Flex",
"roles.COACH": "Entrenador",
"forms.fields.teamTwitter": "Twitter del equipo",
"forms.fields.bio": "Bio",
"forms.fields.uploadImages": "Subir imágenes",
"forms.fields.uploadImages.pfp": "Imagen de perfil",

View File

@ -10,7 +10,7 @@
"stick": "Palanca",
"sens": "Sens",
"weaponPool": "Grupo de armas",
"discordExplanation": "Tu nombre, foto, y cuentas de YouTube, Twitter y Twitch se obtienen por tu cuenta en Discord. Ver <1>FAQ</1> para más información.",
"discordExplanation": "Tu nombre, foto, y cuentas de YouTube, Bluesky y Twitch se obtienen por tu cuenta en Discord. Ver <1>FAQ</1> para más información.",
"favoriteBadge": "Insignia favorita",
"forms.showDiscordUniqueName": "Mostrar usuario de Discord",
@ -35,7 +35,6 @@
"forms.errors.invalidSens": "Motion sens can't be set if R-stick sens isn't",
"forms.info.favoriteBadge": "Tu insignia favorita se muestra en forma grande en tu perfil.",
"search.info": "Buscar usuarios usando su nombre en Discord, Splatoon 3 o Twitter",
"search.noResults": "No se encontraron usuarios que coincidan con '{{query}}'",
"seasons": "Temporadas",

View File

@ -34,7 +34,6 @@
"auth.errors.aborted": "Ingreso cancelado",
"auth.errors.failed": "Ingreso fallido",
"auth.errors.discordPermissions": "Para tu perfil en sendou.ink, el sitio requiere aceso a tu nombre en Discord, avatar, y redes sociales.",
"auth.errors.unknown": "Ingreso fallido debido a un error de servidor. Por favor intenta más tarde o usa /login a travez de Lohi bot. Se puede encontrar en el Discord de sendou.ink:",
"footer.github.subtitle": "Código fuente",
"footer.discord.subtitle": "Ayuda y comentarios",

View File

@ -13,7 +13,7 @@
"q5": "¿Hay un app de sendou.ink?",
"a5": "No, pero sendou.ink se puede instalar como una 'aplicación web progresiva' (PWA) que obtiene muchos de los mismos beneficios, como pantalla completa, un ícono en la pantalla de inicio y un proceso separado. Busque en Google 'cómo instalar PWA en *su navegador*' para encontrar instrucciones.",
"q6": "¿Cómo puedo agregar Twitch/Twitter/YouTube a mi perfil?",
"q6": "¿Cómo puedo agregar Twitch/Bluesky/YouTube a mi perfil?",
"a6": "Usamos tu perfil de Discord para eso. Conéctalos y verifícalos a travéz de Discord. Luego, cierra tu sesión y vuelve a iniciar tu sesión en sendou.ink para actualizar.",
"q7": "¿Cómo puedo agregar videos?",

View File

@ -21,7 +21,6 @@
"roles.FLEX": "Flex",
"roles.SUB": "Suplente",
"roles.COACH": "Entrenador",
"forms.fields.teamTwitter": "Twitter del equipo",
"forms.fields.bio": "Bio",
"forms.fields.uploadImages": "Subir imágenes",
"forms.fields.uploadImages.pfp": "Imagen de perfil",

View File

@ -11,7 +11,7 @@
"stick": "Palanca",
"sens": "Sens",
"weaponPool": "Grupo de armas",
"discordExplanation": "Tu nombre, foto, y cuentas de YouTube, Twitter y Twitch se obtienen por tu cuenta en Discord. Ver <1>FAQ</1> para más información.",
"discordExplanation": "Tu nombre, foto, y cuentas de YouTube, Bluesky y Twitch se obtienen por tu cuenta en Discord. Ver <1>FAQ</1> para más información.",
"favoriteBadge": "Insignia favorita",
"battlefy": "Nombre de cuenta de Battlefy",
@ -43,7 +43,6 @@
"forms.info.favoriteBadge": "Tu insignia favorita se muestra en forma grande en tu perfil.",
"forms.info.battlefy": "El nombre de tu cuenta de Battlefy se utiliza para la clasificación y verificación en algunos torneos.",
"search.info": "Buscar usuarios usando su nombre en Discord, Splatoon 3 o Twitter",
"search.noResults": "No se encontraron usuarios que coincidan con '{{query}}'",
"seasons": "Temporadas",

View File

@ -12,7 +12,7 @@
"q5": "Est-ce qu'il y a une application sendou.ink ?",
"a5": "Non, mais sendou.ink peut être installé en tant 'qu'Application Web Progressive' : vous pourrez bénéficier d'un mode plein écran, d'une icone et d'un processus séparé de votre navigateur. Pour pouvoir l'installer, n'hésitez pas à rechercher 'Comment installer une awp sur *votre natigateur*' sur Google.",
"q6": "Comment puis-je ajouter mes liens Twitch/Twitter/Youtube à mon profil ?",
"q6": "Comment puis-je ajouter mes liens Twitch/Bluesky/Youtube à mon profil ?",
"a6": "Nous utilisons le profil Discord pour ça. Connectez vos comptes sur Discord puis déconnectez et reconnectez vous sur sendou.ink pour les mettre à jour.",
"q7": "Comment puis-je ajouter des vidéos ?",

View File

@ -20,7 +20,6 @@
"roles.BACKLINE": "Backline",
"roles.FLEX": "Flex",
"roles.COACH": "Coach",
"forms.fields.teamTwitter": "Twitter de l'équipe",
"forms.fields.bio": "Bio",
"forms.fields.uploadImages": "Soumettre images",
"forms.fields.uploadImages.pfp": "Emblème",

View File

@ -10,7 +10,7 @@
"stick": "Stick",
"sens": "Sens",
"weaponPool": "Armes jouées",
"discordExplanation": "Votre pseudo, votre photo de profil et vos comptes Youtube, Twitter et Twitch viennent de votre compte Discord. Voir la <1>FAQ</1> pour plus d'informations.",
"discordExplanation": "Votre pseudo, votre photo de profil et vos comptes Youtube, Bluesky et Twitch viennent de votre compte Discord. Voir la <1>FAQ</1> pour plus d'informations.",
"favoriteBadge": "Badge favori",
"forms.showDiscordUniqueName": "Montrer le pseudo Discord",
@ -35,6 +35,5 @@
"forms.errors.invalidSens": "La sensibilité du gyroscope ne peut pas être choisie si la sensibilité du stick droit ne l'est pas",
"forms.info.favoriteBadge": "Votre badge favoris est affiché en grand par défaut sur votre profil.",
"search.info": "Rechercher des utilisateurs par le pseudo Discord, Splatoon 3 ou Twitter",
"search.noResults": "Aucun utilisateur correspondant à '{{query}}' n'a été trouvé"
}

View File

@ -12,7 +12,7 @@
"q5": "Est-ce qu'il y a une application sendou.ink ?",
"a5": "Non, mais sendou.ink peut être installé en tant 'qu'Application Web Progressive' : vous pourrez bénéficier d'un mode plein écran, d'une icone et d'un processus séparé de votre navigateur. Pour pouvoir l'installer, n'hésitez pas à rechercher 'Comment installer une awp sur *votre natigateur*' sur Google.",
"q6": "Comment puis-je ajouter mes liens Twitch/Twitter/Youtube à mon profil ?",
"q6": "Comment puis-je ajouter mes liens Twitch/Bluesky/Youtube à mon profil ?",
"a6": "Nous utilisons le profil Discord pour ça. Connectez vos comptes sur Discord puis déconnectez et reconnectez vous sur sendou.ink pour les mettre à jour.",
"q7": "Comment puis-je ajouter des vidéos ?",

View File

@ -20,7 +20,6 @@
"roles.BACKLINE": "Backline",
"roles.FLEX": "Flex",
"roles.COACH": "Coach",
"forms.fields.teamTwitter": "Twitter de l'équipe",
"forms.fields.bio": "Bio",
"forms.fields.uploadImages": "Soumettre images",
"forms.fields.uploadImages.pfp": "Emblème",

View File

@ -10,7 +10,7 @@
"stick": "Stick",
"sens": "Sens",
"weaponPool": "Armes jouées",
"discordExplanation": "Votre pseudo, votre photo de profil et vos comptes Youtube, Twitter et Twitch viennent de votre compte Discord. Voir la <1>FAQ</1> pour plus d'informations.",
"discordExplanation": "Votre pseudo, votre photo de profil et vos comptes Youtube, Bluesky et Twitch viennent de votre compte Discord. Voir la <1>FAQ</1> pour plus d'informations.",
"favoriteBadge": "Badge favori",
"forms.showDiscordUniqueName": "Montrer le pseudo Discord",
@ -35,6 +35,5 @@
"forms.errors.invalidSens": "La sensibilité du gyroscope ne peut pas être choisie si la sensibilité du stick droit ne l'est pas",
"forms.info.favoriteBadge": "Votre badge favoris est affiché en grand par défaut sur votre profil.",
"search.info": "Rechercher des utilisateurs par le pseudo Discord, Splatoon 3 ou Twitter",
"search.noResults": "Aucun utilisateur correspondant à '{{query}}' n'a été trouvé"
}

View File

@ -12,7 +12,7 @@
"q5": "האם יש אפליקציית sendou.ink?",
"a5": "לא, אבל ניתן להתקין את sendou.ink כ'אפליקציית אינטרנט מתקדמת' שמקבלת הרבה מאותן יתרונות כמו מסך מלא, אייקון במסך הבית ותהליך נפרד מהדפדפן. חפשו בגוגל 'כיצד להתקין PWA על *הדפדפן שלך*' כדי לראות איך.",
"q6": "איך להוסיף חשבון Twitch/Twitter/YouTube לפרופיל שלי?",
"q6": "איך להוסיף חשבון Twitch/Bluesky/YouTube לפרופיל שלי?",
"a6": "אנחנו משתמשים בפרופיל הדיסקורד שלכם בשביל זה. התחברו ואמתו אותם ב-Discord ואז צאו והיכנסו חזרה ל-sendou.ink כדי לעדכן אותם.",
"q7": "איך אני יכול להוסיף סרטונים?",

View File

@ -20,7 +20,6 @@
"roles.BACKLINE": "קו אחורי",
"roles.FLEX": "מעורב",
"roles.COACH": "מאמן",
"forms.fields.teamTwitter": "Twitter של הצוות",
"forms.fields.bio": "ביו",
"forms.fields.uploadImages": "העלאת תמונות",
"forms.fields.uploadImages.pfp": "תמונת פרופיל",

View File

@ -10,7 +10,7 @@
"stick": "סטיק",
"sens": "רגישות",
"weaponPool": "מאגר נשקים",
"discordExplanation": "שם משתמש, תמונת פרופיל, חשבונות YouTube, Twitter ו-Twitch מגיעים מחשבון Discord שלך. ראו <1>שאלות נפוצות</1> למידע נוסף.",
"discordExplanation": "שם משתמש, תמונת פרופיל, חשבונות YouTube, Bluesky ו-Twitch מגיעים מחשבון Discord שלך. ראו <1>שאלות נפוצות</1> למידע נוסף.",
"favoriteBadge": "תג אהוב",
"forms.showDiscordUniqueName": "הראה שם משתמש Discord",
@ -35,6 +35,5 @@
"forms.errors.invalidSens": "לא ניתן להגדיר את רגישות התנועה אם רגישות הסטיק לא מוגדרת",
"forms.info.favoriteBadge": "התג האהוב עליכם מוצג בגדול כברירת מחדל בפרופיל שלכם.",
"search.info": "חיפוש משתמשים לפי שם ב-Discord, Splatoon 3, או Twitter",
"search.noResults": "לא נמצאו משתמשים התואמים '{{query}}'"
}

View File

@ -11,6 +11,6 @@
"q5": "Esiste un app per sendou.ink?",
"a5": "No, ma il sito può essere installato come una 'Progressive Web App', che porta un sacco degli stessi benefici come l'uso a schermo pieno, l'icona sulla schermata home e un processo separato dal browser. Cerca 'Come installare progressive web app su *il tuo browser*' per imparare come si fa.",
"q6": "Come faccio ad aggiungere un link per Twitch/Twitter/Youtube al mio profilo?",
"q6": "Come faccio ad aggiungere un link per Twitch/Bluesky/Youtube al mio profilo?",
"a6": "Il sito sincronizza i profili con le tue connessioni al profilo Discord. Per connettere un account a sendou.ink, connettilo e verificalo su Discord, quindi esci e riaccedi qui per aggiornare il profilo."
}

View File

@ -21,6 +21,5 @@
"forms.errors.invalidCustomUrl.strangeCharacter": "URL personalizzato non può contenere caratteri speciali",
"forms.errors.invalidCustomUrl.duplicate": "URL personalizzato gia in uso da un'altro utente",
"forms.errors.invalidSens": "Sensitività giroscopio non può essere impostata se non hai impostato la sensitività del joystick destro",
"search.info": "Cerca utenti con il nome Discord, Splatoon o Twitter",
"search.noResults": "Nessun utente trovato per '{{query}}'"
}

View File

@ -43,7 +43,6 @@
"auth.errors.aborted": "ログインを中断しました",
"auth.errors.failed": "ログインに失敗しました",
"auth.errors.discordPermissions": "sendou.ink は、Discord のプロファイル名、アバター、SNS連携をサイトのプロファイルに使用します。",
"auth.errors.unknown": "ディスコードのアカウントにバリデートされたイーメールはありますか?助けが必要な場合ディスコードの #helpdeskチャンネルでお聞きください。:",
"footer.github.subtitle": "ソースコード",
"footer.discord.subtitle": "ヘルプ & フィードバック",

View File

@ -14,7 +14,7 @@
"q5": "sendou.ink のアプリはありますか?",
"a5": "いいえ、でも sendou.inkは 'Progressive Web App' としてインストールすることができます。これは、フルスクリーン、ホーム画面へのアイコン、ブラウザからの分離など、アプリと同じ利点があります。'ブラウザへの pwa のインストール方法' でググってみてください。",
"q6": "Twitch/Twitter/YouTube はどうやって自分のプロファイルに追加できますか?",
"q6": "Twitch/Bluesky/YouTube はどうやって自分のプロファイルに追加できますか?",
"a6": "Discord のプロファイルに設定されているものを使用しています。Discord と連携+承認して、表示をリロードするために sendou.ink をログインし直してください。",
"q7": "動画はどうやって追加すればよいですか?",

View File

@ -25,7 +25,6 @@
"roles.SUB": "補欠",
"roles.COACH": "コーチ",
"roles.CHEERLEADER": "引き立て役(チアリーダー)",
"forms.fields.teamTwitter": "チームの X(旧Twitter)",
"forms.fields.teamBsky": "チームの Bluesky",
"forms.fields.bio": "Bio",
"forms.fields.uploadImages": "画像をアップロード",

View File

@ -11,10 +11,9 @@
"stick": "スティック",
"sens": "感度",
"weaponPool": "使用ブキ",
"discordExplanation": "ユーザー名、プロファイル画像、YouTube、Twitter と Twitch アカウントは Discord のアカウントに設定されているものが使用されます。詳しくは <1>FAQ</1> をご覧ください。",
"discordExplanation": "ユーザー名、プロファイル画像、YouTube、Bluesky と Twitch アカウントは Discord のアカウントに設定されているものが使用されます。詳しくは <1>FAQ</1> をご覧ください。",
"favoriteBadge": "お気に入りバッジ",
"battlefy": "Battlefyアカウント名",
"bsky": "Blueskyアカウント名",
"forms.showDiscordUniqueName": "Discord のユーザー名を表示する",
"forms.showDiscordUniqueName.info": "Discord のユニーク名 ({{discordUniqueName}}) 公表しますか?",
@ -43,7 +42,6 @@
"forms.info.favoriteBadge": "お気に入りバッジはデフォルトでプロファイルに大きく表示されます",
"forms.info.battlefy": "Battlefyのアカウント名は特定のトーナメンでシーディング及びにプレイヤー情報の確認に使用されます。",
"search.info": "Discord, Splatoon 3, Twitter アカウント名でユーザーを検索する",
"search.noResults": "該当ユーザーが見つかりません '{{query}}'",
"seasons": "シーズン",

View File

@ -11,6 +11,5 @@
"q5": "Czy jest aplikacja sendou.ink?",
"a5": "Nie, ale sendou.ink może być zainstalowane jako 'Progressive Web App', który ma wiele tych samych korzyści co normalna aplikacja. Wyszukaj 'jak się instaluje PWA na *twojej wyszukiwarce*' by wiedzieć jak.",
"q6": "Jak połączyć moje konto sendou.ink z moim Twitchem/Twitterem/Youtubem?",
"a6": "Używamy do tego twojego profilu Discord. Połącz te konta na Discordzie i odśwież sendou.ink i powinny się pojawić."
}

View File

@ -18,7 +18,6 @@
"roles.SUPPORT": "Support",
"roles.BACKLINE": "Backline",
"roles.COACH": "Trener",
"forms.fields.teamTwitter": "Twitter drużyny",
"forms.fields.bio": "Opis",
"forms.fields.uploadImages": "Wstaw zdjęcia",
"forms.fields.uploadImages.pfp": "Zdjęcie profilowe",

View File

@ -27,6 +27,5 @@
"forms.errors.invalidCustomUrl.duplicate": "Te niestandardowe URl jest już przez kogoś zajęte",
"forms.errors.invalidSens": "Motion sens nie może być ustawione jeśli R-stick sens nie jest",
"search.info": "Wyszukaj kogoś poprzez ich nazwe na Discordzie, Splatoon 3 lub Twitterze",
"search.noResults": "Nie znaleziono użytkownika o nazwie '{{query}}'"
}

View File

@ -34,7 +34,6 @@
"auth.errors.aborted": "Login Abortado",
"auth.errors.failed": "Login Falhou",
"auth.errors.discordPermissions": "Para o seu perfil do sendou.ink, o site precisa de acesso ao nome do perfil do seu Discord, incluindo também o avatar e conexões sociais.",
"auth.errors.unknown": "A sua conta do Discord possui um endereço de e-mail verificado? Para obter ajuda por favor entre em contato conosco (em inglês) pelo canal #helpdesk no nosso Discord:",
"footer.github.subtitle": "Código fonte",
"footer.discord.subtitle": "Ajuda e Feedback",

View File

@ -13,7 +13,7 @@
"q5": "Existe um aplicativo do sendou.ink?",
"a5": "Não mas, o sendou.ink pode ser instalado como um 'Aplicativo Web Progressivo' que possui muitos dos benefícios de um aplicativo tradicional tais como tela cheia, um ícone na tela inicial do seu dispositivo e um processo separado do navegador. Pesquise no Google 'como instalar AWP (PWA em Inglês) no *seu navegador*' para ver como.",
"q6": "Como adicionar o meu Twitch/Twitter/YouTube ao meu perfil?",
"q6": "Como adicionar o meu Twitch/Bluesky/YouTube ao meu perfil?",
"a6": "Nós usamos seu perfil do Discord para isso. Conecte e verifique seus perfis no Discord, depois saia e faça login novamente no sendou.ink para que o seu perfil atualize.",
"q7": "Como posso adicionar vídeos?",

View File

@ -20,7 +20,6 @@
"roles.BACKLINE": "Linha de trás (backline)",
"roles.FLEX": "Flex",
"roles.COACH": "Treinador",
"forms.fields.teamTwitter": "Twitter do Time",
"forms.fields.bio": "Bio",
"forms.fields.uploadImages": "Fazer upload de imagens",
"forms.fields.uploadImages.pfp": "Imagem do perfil",

View File

@ -10,7 +10,7 @@
"stick": "Analógico",
"sens": "Sens",
"weaponPool": "Seleção de armas",
"discordExplanation": "Nome de usuário, foto de perfil, conta do YouTube, Twitter e Twitch vêm da sua conta do Discord. Veja o <1>Perguntas Frequentes</1> para mais informações.",
"discordExplanation": "Nome de usuário, foto de perfil, conta do YouTube, Bluesky e Twitch vêm da sua conta do Discord. Veja o <1>Perguntas Frequentes</1> para mais informações.",
"favoriteBadge": "Insígnia Favorita",
"forms.showDiscordUniqueName": "Mostrar nome de usuário Discord",
@ -35,7 +35,6 @@
"forms.errors.invalidSens": "A sensibilidade de Movimento não pode ser definida se a sensibilidade do Analógico Direito não está",
"forms.info.favoriteBadge": "Por padrão, sua insígnia favorita é mostrada em tamanho maior no seu perfil.",
"search.info": "Procurar por usuários pelo nome do Discord, Splatoon 3 ou Twitter",
"search.noResults": "Nenhum usuário encontrado com o termo '{{query}}'",
"seasons": "Temporadas",

View File

@ -11,6 +11,6 @@
"q5": "Существует ли приложение sendou.ink?",
"a5": "Нет, но sendou.ink может быть установлен как 'прогрессивное веб-приложение', которое дает такие преимущества как полноэкранный режим, иконка на домашнем экране и отдельный процесс от браузера. Для инструкции по установке вы можете указать в поисковике 'как установить pwa для *ваш браузер*'.",
"q6": "Как добавить ссылку на Twitch/Twitter/YouTube в моём профиле?",
"q6": "Как добавить ссылку на Twitch/Bluesky/YouTube в моём профиле?",
"a6": "Мы используем ваш профиль Discord для этого. Подключите и подтвердите нужный аккаунт в Discord, а затем выйдите и зайдите снова на sendou.ink для обновления ссылок."
}

View File

@ -20,7 +20,6 @@
"roles.BACKLINE": "Бэклайн",
"roles.FLEX": "Флекс",
"roles.COACH": "Тренер",
"forms.fields.teamTwitter": "Твиттер команды",
"forms.fields.bio": "Описание",
"forms.fields.uploadImages": "Загрузить изображения",
"forms.fields.uploadImages.pfp": "Аватар команды",

View File

@ -10,7 +10,7 @@
"stick": "Стик",
"sens": "Чувствительность",
"weaponPool": "Используемое оружие",
"discordExplanation": "Имя пользователя, аватар, ссылка на аккаунты YouTube, Twitter и Twitch берутся из вашего аккаунта в Discord. Посмотрите <1>FAQ</1> для дополнительной информации.",
"discordExplanation": "Имя пользователя, аватар, ссылка на аккаунты YouTube, Bluesky и Twitch берутся из вашего аккаунта в Discord. Посмотрите <1>FAQ</1> для дополнительной информации.",
"favoriteBadge": "Любимый значок",
"forms.showDiscordUniqueName": "Показать пользовательское имя Discord",
@ -35,7 +35,6 @@
"forms.errors.invalidSens": "Чувствительность наклона не может быть указана, если не указана чувствительность стика",
"forms.info.favoriteBadge": "По умолчанию ваш любимый значок отображён большим в вашем профиле.",
"search.info": "Поиск пользователей по имени в Discord, Splatoon 3 или Twitter",
"search.noResults": "Не найден пользователь по запросу '{{query}}'",
"seasons": "Сезоны",

View File

@ -34,7 +34,6 @@
"auth.errors.aborted": "登录中止",
"auth.errors.failed": "登录失败",
"auth.errors.discordPermissions": "为了完善您的sendou.ink个人资料网站需要获取您的Discord名字、头像和社交链接。",
"auth.errors.unknown": "您的Discord账号验证过邮箱吗请在我们的Discord服务器的#helpdesk频道获取帮助",
"footer.github.subtitle": "源代码",
"footer.discord.subtitle": "帮助及反馈",

View File

@ -13,7 +13,7 @@
"q5": "sendou.ink有专门的APP吗",
"a5": "没有但是可以将sendou.ink下载为渐进式网络应用程序PWA这样就能获得全屏使用、主页图标以及和网页端不同的操作方式。搜索如何在您的浏览器下载PWA来查看具体操作步骤。",
"q6": "如何在我的个人资料里添加Twitch/Twitter/YouTube",
"q6": "如何在我的个人资料里添加Twitch/Bluesky/YouTube",
"a6": "我们通过您的Discord资料来添加这些信息。将这些账号和您的Discord绑定登出再登录sendou.ink后即可更新。",
"q7": "我应该如何添加视频?",

View File

@ -20,7 +20,6 @@
"roles.BACKLINE": "后排",
"roles.FLEX": "自由人",
"roles.COACH": "教练",
"forms.fields.teamTwitter": "队伍Twitter",
"forms.fields.bio": "简介",
"forms.fields.uploadImages": "上传图片",
"forms.fields.uploadImages.pfp": "头像",

View File

@ -11,7 +11,7 @@
"stick": "摇杆",
"sens": "感度",
"weaponPool": "武器池",
"discordExplanation": "用户名、头像、Youtube、Twitter和Twitch账号皆来自您的Discord账号。查看 <1>FAQ</1> 以获得更多相关信息。",
"discordExplanation": "用户名、头像、Youtube、Bluesky和Twitch账号皆来自您的Discord账号。查看 <1>FAQ</1> 以获得更多相关信息。",
"favoriteBadge": "最喜爱的徽章",
"battlefy": "Battlefy用户名",
@ -42,7 +42,6 @@
"forms.info.favoriteBadge": "您最喜爱的徽章将会默认在资料页展示为大徽章",
"forms.info.battlefy": "Battlefy用户名会在部分比赛被用于种子排名和验证",
"search.info": "通过Discord、斯普拉遁3或Twitter名称查找用户",
"search.noResults": "没有符合 '{{query}}' 的用户",
"seasons": "赛季",

View File

@ -0,0 +1,8 @@
export function up(db) {
db.transaction(() => {
db.prepare(/* sql */ `alter table "User" drop column "twitter"`).run();
db.prepare(/* sql */ `alter table "AllTeam" drop column "twitter"`).run();
db.prepare(/* sql */ `update "User" set "bsky" = null`).run();
})();
}

126
package-lock.json generated
View File

@ -60,8 +60,8 @@
"react-use": "^17.6.0",
"react-use-draggable-scroll": "^0.4.7",
"reconnecting-websocket": "^4.4.0",
"remix-auth": "^3.7.0",
"remix-auth-oauth2": "^1.11.2",
"remix-auth": "^4.1.0",
"remix-auth-oauth2": "^3.2.2",
"remix-i18next": "^6.4.1",
"remix-utils": "^7.7.0",
"slugify": "^1.6.6",
@ -1864,6 +1864,18 @@
"react": ">=16.8.0"
}
},
"node_modules/@edgefirst-dev/data": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@edgefirst-dev/data/-/data-0.0.2.tgz",
"integrity": "sha512-kvw7RRDW9TJtOYHhzMYQ4g9x+ji87o02CROTnS9qCl2SZXauAPRpTiIQMGC5YLwvgzxV76Y2uMavQK4TQB1z0Q==",
"funding": [
"https://github.com/sponsors/sergiodxa"
],
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
@ -2583,6 +2595,12 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/@mjackson/headers": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@mjackson/headers/-/headers-0.9.0.tgz",
"integrity": "sha512-1WFCu2iRaqbez9hcYYI611vcH1V25R+fDfOge/CyKc8sdbzniGfy/FRhNd3DgvFF4ZEEX2ayBrvFHLtOpfvadw==",
"license": "MIT"
},
"node_modules/@npmcli/fs": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz",
@ -2658,6 +2676,52 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/@oslojs/asn1": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@oslojs/asn1/-/asn1-1.0.0.tgz",
"integrity": "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==",
"license": "MIT",
"dependencies": {
"@oslojs/binary": "1.0.0"
}
},
"node_modules/@oslojs/binary": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@oslojs/binary/-/binary-1.0.0.tgz",
"integrity": "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==",
"license": "MIT"
},
"node_modules/@oslojs/crypto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@oslojs/crypto/-/crypto-1.0.1.tgz",
"integrity": "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==",
"license": "MIT",
"dependencies": {
"@oslojs/asn1": "1.0.0",
"@oslojs/binary": "1.0.0"
}
},
"node_modules/@oslojs/encoding": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz",
"integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==",
"license": "MIT"
},
"node_modules/@oslojs/jwt": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@oslojs/jwt/-/jwt-0.2.0.tgz",
"integrity": "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg==",
"license": "MIT",
"dependencies": {
"@oslojs/encoding": "0.4.1"
}
},
"node_modules/@oslojs/jwt/node_modules/@oslojs/encoding": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-0.4.1.tgz",
"integrity": "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==",
"license": "MIT"
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@ -7881,6 +7945,17 @@
"node": ">= 8"
}
},
"node_modules/arctic": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/arctic/-/arctic-3.2.1.tgz",
"integrity": "sha512-lvOfv088aiaNCN4bGpnVd0cJmQKUkB2gGaZeA3lTwbBZVj69+zd/LpBPO6CXNCyX45CKqqcaOV6J7HeJ1k4hEA==",
"license": "MIT",
"dependencies": {
"@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0",
"@oslojs/jwt": "0.2.0"
}
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@ -14625,39 +14700,36 @@
"license": "MIT"
},
"node_modules/remix-auth": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-3.7.0.tgz",
"integrity": "sha512-2QVjp2nJVaYxuFBecMQwzixCO7CLSssttLBU5eVlNcNlVeNMmY1g7OkmZ1Ogw9sBcoMXZ18J7xXSK0AISVFcfQ==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-4.1.0.tgz",
"integrity": "sha512-Xdy42clt+g79GCn+Wl1+B6S/yWnvrStnk62vo1pGuRuUwHC+pjmeEE52ZRkRPuhLGqsQnoDD9TVz/wfEJAGF8g==",
"funding": [
"https://github.com/sponsors/sergiodxa"
],
"license": "MIT",
"dependencies": {
"uuid": "^8.3.2"
},
"peerDependencies": {
"@remix-run/react": "^1.0.0 || ^2.0.0",
"@remix-run/server-runtime": "^1.0.0 || ^2.0.0"
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/remix-auth-oauth2": {
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/remix-auth-oauth2/-/remix-auth-oauth2-1.11.2.tgz",
"integrity": "sha512-5ORP+LMi5CVCA/Wb8Z+FCAJ73Uiy4uyjEzhlVwNBfdAkPOnfxzoi+q/pY/CrueYv3OniCXRM35ZYqkVi3G1UPw==",
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/remix-auth-oauth2/-/remix-auth-oauth2-3.2.2.tgz",
"integrity": "sha512-m4jb7ZTFau4UcTEfuZQ+SPrXvXQSR8nrcAxww4ddxyvGHZglSeSYPqdwC81gMeEgIxDEgHULZgRfklV4wqJ4fA==",
"funding": [
"https://github.com/sponsors/sergiodxa"
],
"license": "MIT",
"dependencies": {
"debug": "^4.3.4",
"uuid": "^9.0.1"
"@edgefirst-dev/data": "^0.0.2",
"@mjackson/headers": "^0.9.0",
"arctic": "^3.0.0",
"debug": "^4.3.7"
},
"engines": {
"node": "^20.0.0 || >=20.0.0"
},
"peerDependencies": {
"@remix-run/server-runtime": "^1.0.0 || ^2.0.0",
"remix-auth": "^3.6.0"
}
},
"node_modules/remix-auth/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"remix-auth": "^4.0.0"
}
},
"node_modules/remix-i18next": {

View File

@ -76,8 +76,8 @@
"react-use": "^17.6.0",
"react-use-draggable-scroll": "^0.4.7",
"reconnecting-websocket": "^4.4.0",
"remix-auth": "^3.7.0",
"remix-auth-oauth2": "^1.11.2",
"remix-auth": "^4.1.0",
"remix-auth-oauth2": "^3.2.2",
"remix-i18next": "^6.4.1",
"remix-utils": "^7.7.0",
"slugify": "^1.6.6",