();
+ const [value, setValue] = React.useState(team.bsky ?? "");
+
+ return (
+
+
+ setValue(e.target.value)}
+ />
+
+ );
+}
+
function BioTextarea() {
const { t } = useTranslation(["team"]);
const { team } = useLoaderData();
diff --git a/app/features/team/routes/t.$customUrl.tsx b/app/features/team/routes/t.$customUrl.tsx
index 019e22eeb4..b7a414a70a 100644
--- a/app/features/team/routes/t.$customUrl.tsx
+++ b/app/features/team/routes/t.$customUrl.tsx
@@ -10,6 +10,7 @@ import { FormWithConfirm } from "~/components/FormWithConfirm";
import { WeaponImage } from "~/components/Image";
import { Main } from "~/components/Main";
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";
@@ -21,6 +22,7 @@ import type { SendouRouteHandle } from "~/utils/remix";
import { makeTitle } from "~/utils/strings";
import {
TEAM_SEARCH_PAGE,
+ bskyUrl,
editTeamPage,
manageTeamRosterPage,
navIconUrl,
@@ -126,7 +128,7 @@ function TeamBanner() {
})}
- {team.name}
+ {team.name}
{team.avatarSrc ? : null}
@@ -151,6 +153,7 @@ function MobileTeamNameCountry() {
{team.name}
+
);
@@ -174,6 +177,23 @@ function TwitterLink({ testId }: { testId?: string }) {
);
}
+function BskyLink() {
+ const { team } = useLoaderData();
+
+ if (!team.bsky) return null;
+
+ return (
+
+
+
+ );
+}
+
function ActionButtons() {
const { t } = useTranslation(["team"]);
const user = useUser();
diff --git a/app/features/team/team-constants.ts b/app/features/team/team-constants.ts
index 323ef51834..df5a0232ac 100644
--- a/app/features/team/team-constants.ts
+++ b/app/features/team/team-constants.ts
@@ -3,6 +3,7 @@ export const TEAM = {
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,
MAX_TEAM_COUNT_PATRON: 5,
diff --git a/app/features/team/team-schemas.server.ts b/app/features/team/team-schemas.server.ts
index fa575f0f63..243bcf54c5 100644
--- a/app/features/team/team-schemas.server.ts
+++ b/app/features/team/team-schemas.server.ts
@@ -32,6 +32,10 @@ export const editTeamSchema = z.union([
falsyToNull,
z.string().max(TEAM.TWITTER_MAX_LENGTH).nullable(),
),
+ bsky: z.preprocess(
+ falsyToNull,
+ z.string().max(TEAM.BSKY_MAX_LENGTH).nullable(),
+ ),
css: z.preprocess(falsyToNull, z.string().refine(jsonParseable).nullable()),
}),
]);
diff --git a/app/features/team/team.css b/app/features/team/team.css
index e0223a5e2d..11ce2fc7ac 100644
--- a/app/features/team/team.css
+++ b/app/features/team/team.css
@@ -97,6 +97,26 @@
fill: #1da1f2;
}
+.team__bsky-link {
+ padding: var(--s-1);
+ border: 1px solid;
+ border-radius: 50%;
+ border-color: #1285fe;
+ background-color: #1285fe2f;
+ height: 24.4px;
+ width: 24.4px;
+ display: grid;
+ place-items: center;
+}
+
+.team__bsky-link > svg {
+ width: 0.9rem;
+}
+
+.team__bsky-link path {
+ fill: #1285fe;
+}
+
.team__banner__avatar {
grid-area: avatar;
align-self: flex-end;
diff --git a/app/features/tournament-organization/components/SocialLinksList.tsx b/app/features/tournament-organization/components/SocialLinksList.tsx
index f2a58319c5..bceb4b8476 100644
--- a/app/features/tournament-organization/components/SocialLinksList.tsx
+++ b/app/features/tournament-organization/components/SocialLinksList.tsx
@@ -1,4 +1,5 @@
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";
@@ -24,6 +25,7 @@ function SocialLink({ url }: { url: string }) {
youtube: type === "youtube",
twitter: type === "twitter",
twitch: type === "twitch",
+ bsky: type === "bsky",
})}
>
@@ -48,6 +50,10 @@ function SocialLinkIcon({ url }: { url: string }) {
return ;
}
+ if (type === "bsky") {
+ return ;
+ }
+
return ;
}
@@ -64,5 +70,9 @@ const urlToLinkType = (url: string) => {
return "youtube";
}
+ if (url.includes("bsky.app")) {
+ return "bsky";
+ }
+
return null;
};
diff --git a/app/features/tournament-organization/tournament-organization.css b/app/features/tournament-organization/tournament-organization.css
index a51b21b32a..97270b1bc2 100644
--- a/app/features/tournament-organization/tournament-organization.css
+++ b/app/features/tournament-organization/tournament-organization.css
@@ -181,3 +181,7 @@
.org__social-link__icon-container.twitter svg {
fill: #1da1f2;
}
+
+.org__social-link__icon-container.bsky path {
+ fill: #1285fe;
+}
diff --git a/app/features/user-page/UserRepository.server.ts b/app/features/user-page/UserRepository.server.ts
index af3f9776e7..a025956e02 100644
--- a/app/features/user-page/UserRepository.server.ts
+++ b/app/features/user-page/UserRepository.server.ts
@@ -140,6 +140,7 @@ export async function findProfileByIdentifier(
"User.twitter",
"User.youtubeId",
"User.battlefy",
+ "User.bsky",
"User.country",
"User.bio",
"User.motionSens",
@@ -586,6 +587,7 @@ type UpdateProfileArgs = Pick<
| "stickSens"
| "inGameName"
| "battlefy"
+ | "bsky"
| "css"
| "favoriteBadgeId"
| "showDiscordUniqueName"
@@ -628,6 +630,7 @@ 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,
diff --git a/app/features/user-page/routes/u.$identifier.edit.tsx b/app/features/user-page/routes/u.$identifier.edit.tsx
index 19575461c0..28cb5de7be 100644
--- a/app/features/user-page/routes/u.$identifier.edit.tsx
+++ b/app/features/user-page/routes/u.$identifier.edit.tsx
@@ -91,6 +91,10 @@ 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
@@ -257,6 +261,7 @@ export default function UserEditPage() {
+
@@ -455,6 +460,24 @@ function BattlefyInput() {
);
}
+function BskyInput() {
+ const { t } = useTranslation(["user"]);
+ const data = useLoaderData();
+
+ return (
+
+
+
+
+ );
+}
+
function WeaponPoolSelect() {
const data = useLoaderData();
const [weapons, setWeapons] = React.useState(data.user.weapons);
diff --git a/app/features/user-page/routes/u.$identifier.index.tsx b/app/features/user-page/routes/u.$identifier.index.tsx
index 57f11c2e29..b0997d4ad5 100644
--- a/app/features/user-page/routes/u.$identifier.index.tsx
+++ b/app/features/user-page/routes/u.$identifier.index.tsx
@@ -4,7 +4,9 @@ import { useTranslation } from "react-i18next";
import { Avatar } from "~/components/Avatar";
import { Flag } from "~/components/Flag";
import { Image, WeaponImage } from "~/components/Image";
+import { Popover } from "~/components/Popover";
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";
@@ -17,6 +19,7 @@ import type { SendouRouteHandle } from "~/utils/remix";
import { rawSensToString } from "~/utils/strings";
import { assertUnreachable } from "~/utils/types";
import {
+ bskyUrl,
modeImageUrl,
navIconUrl,
teamPage,
@@ -25,7 +28,6 @@ import {
} from "~/utils/urls";
import type { UserPageLoaderData } from "./u.$identifier";
-import { Popover } from "~/components/Popover";
import { loader } from "../loaders/u.$identifier.index.server";
export { loader };
@@ -67,6 +69,9 @@ export default function UserInfoPage() {
{data.user.battlefy ? (
) : null}
+ {data.user.bsky ? (
+
+ ) : null}
@@ -168,7 +173,7 @@ function SecondaryTeamsPopover() {
}
interface SocialLinkProps {
- type: "youtube" | "twitter" | "twitch" | "battlefy";
+ type: "youtube" | "twitter" | "twitch" | "battlefy" | "bsky";
identifier: string;
}
@@ -176,7 +181,7 @@ export function SocialLink({
type,
identifier,
}: {
- type: "youtube" | "twitter" | "twitch" | "battlefy";
+ type: SocialLinkProps["type"];
identifier: string;
}) {
const href = () => {
@@ -189,6 +194,8 @@ export function SocialLink({
return `https://www.youtube.com/channel/${identifier}`;
case "battlefy":
return `https://battlefy.com/users/${identifier}`;
+ case "bsky":
+ return bskyUrl(identifier);
default:
assertUnreachable(type);
}
@@ -201,6 +208,7 @@ export function SocialLink({
twitter: type === "twitter",
twitch: type === "twitch",
battlefy: type === "battlefy",
+ bsky: type === "bsky",
})}
href={href()}
>
@@ -219,6 +227,8 @@ function SocialLinkIcon({ type }: Pick) {
return ;
case "battlefy":
return ;
+ case "bsky":
+ return ;
default:
assertUnreachable(type);
}
diff --git a/app/styles/u.css b/app/styles/u.css
index 63b25e81f2..0c6a2d530e 100644
--- a/app/styles/u.css
+++ b/app/styles/u.css
@@ -97,6 +97,17 @@
fill: #de4c5e;
}
+.u__social-link.bsky {
+ border-color: #1285fe;
+ background-color: #1285fe2f;
+ display: grid;
+ place-items: center;
+}
+
+.u__social-link.bsky path {
+ fill: #1285fe;
+}
+
.u__extra-infos {
display: flex;
max-width: 24rem;
diff --git a/app/utils/urls.ts b/app/utils/urls.ts
index ab340e4195..1fe582c194 100644
--- a/app/utils/urls.ts
+++ b/app/utils/urls.ts
@@ -73,6 +73,8 @@ export const SPR_INFO_URL =
export const twitterUrl = (accountName: string) =>
`https://twitter.com/${accountName}`;
+export const bskyUrl = (accountName: string) =>
+ `https://bsky.app/profile/${accountName}`;
export const twitchUrl = (accountName: string) =>
`https://twitch.tv/${accountName}`;
diff --git a/locales/en/team.json b/locales/en/team.json
index 293fba50d8..7f25474883 100644
--- a/locales/en/team.json
+++ b/locales/en/team.json
@@ -27,6 +27,7 @@
"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",
"forms.fields.uploadImages.pfp": "Profile Picture",
diff --git a/locales/en/user.json b/locales/en/user.json
index aeff411bf9..41c24e407d 100644
--- a/locales/en/user.json
+++ b/locales/en/user.json
@@ -14,6 +14,7 @@
"discordExplanation": "Username, profile picture, YouTube, Twitter and Twitch accounts come from your Discord account. See <1>FAQ1> 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?",
diff --git a/migrations/070-bsky.js b/migrations/070-bsky.js
new file mode 100644
index 0000000000..998d38b3a2
--- /dev/null
+++ b/migrations/070-bsky.js
@@ -0,0 +1,7 @@
+export function up(db) {
+ db.transaction(() => {
+ db.prepare(/* sql */ `alter table "User" add "bsky" text`).run();
+
+ db.prepare(/* sql */ `alter table "AllTeam" add "bsky" text`).run();
+ })();
+}