mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-26 17:27:09 -05:00
Bluesky for user & team pages (#1891)
* Migrations * Bluesky for team page * Unify interfaces * For user page * To org social links
This commit is contained in:
parent
803b93e061
commit
59d77642e5
16
app/components/icons/Bsky.tsx
Normal file
16
app/components/icons/Bsky.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
export function BskyIcon({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={className}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 568 501"
|
||||||
|
>
|
||||||
|
<title>Bluesky butterfly logo</title>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M123.121 33.664C188.241 82.553 258.281 181.68 284 234.873c25.719-53.192 95.759-152.32 160.879-201.21C491.866-1.611 568-28.906 568 57.947c0 17.346-9.945 145.713-15.778 166.555-20.275 72.453-94.155 90.933-159.875 79.748C507.222 323.8 536.444 388.56 473.333 453.32c-119.86 122.992-172.272-30.859-185.702-70.281-2.462-7.227-3.614-10.608-3.631-7.733-.017-2.875-1.169.506-3.631 7.733-13.43 39.422-65.842 193.273-185.702 70.281-63.111-64.76-33.89-129.52 80.986-149.071-65.72 11.185-139.6-7.295-159.875-79.748C9.945 203.659 0 75.291 0 57.946 0-28.906 76.135-1.612 123.121 33.664Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ export const USER = {
|
||||||
CUSTOM_URL_MAX_LENGTH: 32,
|
CUSTOM_URL_MAX_LENGTH: 32,
|
||||||
CUSTOM_NAME_MAX_LENGTH: 32,
|
CUSTOM_NAME_MAX_LENGTH: 32,
|
||||||
BATTLEFY_MAX_LENGTH: 32,
|
BATTLEFY_MAX_LENGTH: 32,
|
||||||
|
BSKY_MAX_LENGTH: 50,
|
||||||
IN_GAME_NAME_TEXT_MAX_LENGTH: 20,
|
IN_GAME_NAME_TEXT_MAX_LENGTH: 20,
|
||||||
IN_GAME_NAME_DISCRIMINATOR_MAX_LENGTH: 5,
|
IN_GAME_NAME_DISCRIMINATOR_MAX_LENGTH: 5,
|
||||||
WEAPON_POOL_MAX_SIZE: 5,
|
WEAPON_POOL_MAX_SIZE: 5,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||||
? ColumnType<S, I | undefined, U>
|
? ColumnType<S, I | undefined, U>
|
||||||
: ColumnType<T, T | undefined, T>;
|
: ColumnType<T, T | undefined, T>;
|
||||||
|
|
||||||
export interface AllTeam {
|
export type MemberRole = (typeof TEAM_MEMBER_ROLES)[number];
|
||||||
|
|
||||||
|
export interface Team {
|
||||||
avatarImgId: number | null;
|
avatarImgId: number | null;
|
||||||
bannerImgId: number | null;
|
bannerImgId: number | null;
|
||||||
bio: string | null;
|
bio: string | null;
|
||||||
|
|
@ -32,11 +34,10 @@ export interface AllTeam {
|
||||||
inviteCode: string;
|
inviteCode: string;
|
||||||
name: string;
|
name: string;
|
||||||
twitter: string | null;
|
twitter: string | null;
|
||||||
|
bsky: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MemberRole = (typeof TEAM_MEMBER_ROLES)[number];
|
export interface TeamMember {
|
||||||
|
|
||||||
export interface AllTeamMember {
|
|
||||||
createdAt: Generated<number>;
|
createdAt: Generated<number>;
|
||||||
isOwner: Generated<number>;
|
isOwner: Generated<number>;
|
||||||
leftAt: number | null;
|
leftAt: number | null;
|
||||||
|
|
@ -46,20 +47,6 @@ export interface AllTeamMember {
|
||||||
isMainTeam: number;
|
isMainTeam: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Team {
|
|
||||||
avatarImgId: number | null;
|
|
||||||
bannerImgId: number | null;
|
|
||||||
bio: string | null;
|
|
||||||
createdAt: number | null;
|
|
||||||
css: ColumnType<Record<string, string> | null, string | null, string | null>;
|
|
||||||
customUrl: string;
|
|
||||||
deletedAt: number | null;
|
|
||||||
id: GeneratedAlways<number>;
|
|
||||||
inviteCode: string;
|
|
||||||
name: string;
|
|
||||||
twitter: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Art {
|
export interface Art {
|
||||||
authorId: number;
|
authorId: number;
|
||||||
createdAt: Generated<number>;
|
createdAt: Generated<number>;
|
||||||
|
|
@ -759,6 +746,7 @@ export interface User {
|
||||||
stickSens: number | null;
|
stickSens: number | null;
|
||||||
twitch: string | null;
|
twitch: string | null;
|
||||||
twitter: string | null;
|
twitter: string | null;
|
||||||
|
bsky: string | null;
|
||||||
battlefy: string | null;
|
battlefy: string | null;
|
||||||
vc: Generated<"YES" | "NO" | "LISTEN_ONLY">;
|
vc: Generated<"YES" | "NO" | "LISTEN_ONLY">;
|
||||||
youtubeId: string | null;
|
youtubeId: string | null;
|
||||||
|
|
@ -848,8 +836,8 @@ export type Tables = { [P in keyof DB]: Selectable<DB[P]> };
|
||||||
export type TablesInsertable = { [P in keyof DB]: Insertable<DB[P]> };
|
export type TablesInsertable = { [P in keyof DB]: Insertable<DB[P]> };
|
||||||
|
|
||||||
export interface DB {
|
export interface DB {
|
||||||
AllTeam: AllTeam;
|
AllTeam: Team;
|
||||||
AllTeamMember: AllTeamMember;
|
AllTeamMember: TeamMember;
|
||||||
Art: Art;
|
Art: Art;
|
||||||
ArtTag: ArtTag;
|
ArtTag: ArtTag;
|
||||||
ArtUserMetadata: ArtUserMetadata;
|
ArtUserMetadata: ArtUserMetadata;
|
||||||
|
|
@ -887,8 +875,8 @@ export interface DB {
|
||||||
SplatoonPlayer: SplatoonPlayer;
|
SplatoonPlayer: SplatoonPlayer;
|
||||||
TaggedArt: TaggedArt;
|
TaggedArt: TaggedArt;
|
||||||
Team: Team;
|
Team: Team;
|
||||||
TeamMember: AllTeamMember;
|
TeamMember: TeamMember;
|
||||||
TeamMemberWithSecondary: AllTeamMember;
|
TeamMemberWithSecondary: TeamMember;
|
||||||
Tournament: Tournament;
|
Tournament: Tournament;
|
||||||
TournamentStaff: TournamentStaff;
|
TournamentStaff: TournamentStaff;
|
||||||
TournamentBadgeOwner: TournamentBadgeOwner;
|
TournamentBadgeOwner: TournamentBadgeOwner;
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ export function findByCustomUrl(customUrl: string) {
|
||||||
"Team.id",
|
"Team.id",
|
||||||
"Team.name",
|
"Team.name",
|
||||||
"Team.twitter",
|
"Team.twitter",
|
||||||
|
"Team.bsky",
|
||||||
"Team.bio",
|
"Team.bio",
|
||||||
"Team.customUrl",
|
"Team.customUrl",
|
||||||
"Team.css",
|
"Team.css",
|
||||||
|
|
@ -140,6 +141,33 @@ export async function create(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function update({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
customUrl,
|
||||||
|
bio,
|
||||||
|
twitter,
|
||||||
|
bsky,
|
||||||
|
css,
|
||||||
|
}: Pick<
|
||||||
|
Insertable<Tables["Team"]>,
|
||||||
|
"id" | "name" | "customUrl" | "bio" | "twitter" | "bsky"
|
||||||
|
> & { css: string | null }) {
|
||||||
|
return db
|
||||||
|
.updateTable("AllTeam")
|
||||||
|
.set({
|
||||||
|
name,
|
||||||
|
customUrl,
|
||||||
|
bio,
|
||||||
|
twitter,
|
||||||
|
bsky,
|
||||||
|
css,
|
||||||
|
})
|
||||||
|
.where("id", "=", id)
|
||||||
|
.returningAll()
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
export function switchMainTeam({
|
export function switchMainTeam({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { sql } from "~/db/sql";
|
|
||||||
import type { Team } from "~/db/types";
|
|
||||||
|
|
||||||
const stm = sql.prepare(/*sql*/ `
|
|
||||||
update "AllTeam"
|
|
||||||
set
|
|
||||||
"name" = @name,
|
|
||||||
"customUrl" = @customUrl,
|
|
||||||
"bio" = @bio,
|
|
||||||
"twitter" = @twitter,
|
|
||||||
"css" = @css
|
|
||||||
where "id" = @id
|
|
||||||
returning *
|
|
||||||
`);
|
|
||||||
|
|
||||||
export function edit({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
customUrl,
|
|
||||||
bio,
|
|
||||||
twitter,
|
|
||||||
css,
|
|
||||||
}: Pick<Team, "id" | "name" | "customUrl" | "bio" | "twitter" | "css">) {
|
|
||||||
return stm.get({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
customUrl,
|
|
||||||
bio,
|
|
||||||
twitter,
|
|
||||||
css,
|
|
||||||
}) as Team;
|
|
||||||
}
|
|
||||||
|
|
@ -37,7 +37,6 @@ import {
|
||||||
} from "~/utils/urls";
|
} from "~/utils/urls";
|
||||||
import * as TeamRepository from "../TeamRepository.server";
|
import * as TeamRepository from "../TeamRepository.server";
|
||||||
import { deleteTeam } from "../queries/deleteTeam.server";
|
import { deleteTeam } from "../queries/deleteTeam.server";
|
||||||
import { edit } from "../queries/edit.server";
|
|
||||||
import { TEAM } from "../team-constants";
|
import { TEAM } from "../team-constants";
|
||||||
import { editTeamSchema, teamParamsSchema } from "../team-schemas.server";
|
import { editTeamSchema, teamParamsSchema } from "../team-schemas.server";
|
||||||
import { canAddCustomizedColors, isTeamOwner } from "../team-utils";
|
import { canAddCustomizedColors, isTeamOwner } from "../team-utils";
|
||||||
|
|
@ -106,7 +105,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const editedTeam = edit({
|
const editedTeam = await TeamRepository.update({
|
||||||
id: team.id,
|
id: team.id,
|
||||||
customUrl: newCustomUrl,
|
customUrl: newCustomUrl,
|
||||||
...data,
|
...data,
|
||||||
|
|
@ -158,6 +157,7 @@ export default function EditTeamPage() {
|
||||||
) : null}
|
) : null}
|
||||||
<NameInput />
|
<NameInput />
|
||||||
<TwitterInput />
|
<TwitterInput />
|
||||||
|
<BlueskyInput />
|
||||||
<BioTextarea />
|
<BioTextarea />
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
|
|
@ -249,6 +249,26 @@ function TwitterInput() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BlueskyInput() {
|
||||||
|
const { t } = useTranslation(["team"]);
|
||||||
|
const { team } = useLoaderData<typeof loader>();
|
||||||
|
const [value, setValue] = React.useState(team.bsky ?? "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="bsky">{t("team:forms.fields.teamBsky")}</Label>
|
||||||
|
<Input
|
||||||
|
leftAddon="https://bsky.app/profile/"
|
||||||
|
id="bsky"
|
||||||
|
name="bsky"
|
||||||
|
maxLength={TEAM.BSKY_MAX_LENGTH}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function BioTextarea() {
|
function BioTextarea() {
|
||||||
const { t } = useTranslation(["team"]);
|
const { t } = useTranslation(["team"]);
|
||||||
const { team } = useLoaderData<typeof loader>();
|
const { team } = useLoaderData<typeof loader>();
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { FormWithConfirm } from "~/components/FormWithConfirm";
|
||||||
import { WeaponImage } from "~/components/Image";
|
import { WeaponImage } from "~/components/Image";
|
||||||
import { Main } from "~/components/Main";
|
import { Main } from "~/components/Main";
|
||||||
import { SubmitButton } from "~/components/SubmitButton";
|
import { SubmitButton } from "~/components/SubmitButton";
|
||||||
|
import { BskyIcon } from "~/components/icons/Bsky";
|
||||||
import { EditIcon } from "~/components/icons/Edit";
|
import { EditIcon } from "~/components/icons/Edit";
|
||||||
import { StarIcon } from "~/components/icons/Star";
|
import { StarIcon } from "~/components/icons/Star";
|
||||||
import { TwitterIcon } from "~/components/icons/Twitter";
|
import { TwitterIcon } from "~/components/icons/Twitter";
|
||||||
|
|
@ -21,6 +22,7 @@ import type { SendouRouteHandle } from "~/utils/remix";
|
||||||
import { makeTitle } from "~/utils/strings";
|
import { makeTitle } from "~/utils/strings";
|
||||||
import {
|
import {
|
||||||
TEAM_SEARCH_PAGE,
|
TEAM_SEARCH_PAGE,
|
||||||
|
bskyUrl,
|
||||||
editTeamPage,
|
editTeamPage,
|
||||||
manageTeamRosterPage,
|
manageTeamRosterPage,
|
||||||
navIconUrl,
|
navIconUrl,
|
||||||
|
|
@ -126,7 +128,7 @@ function TeamBanner() {
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="team__banner__name">
|
<div className="team__banner__name">
|
||||||
{team.name} <TwitterLink testId="twitter-link" />
|
{team.name} <TwitterLink testId="twitter-link" /> <BskyLink />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{team.avatarSrc ? <div className="team__banner__avatar__spacer" /> : null}
|
{team.avatarSrc ? <div className="team__banner__avatar__spacer" /> : null}
|
||||||
|
|
@ -151,6 +153,7 @@ function MobileTeamNameCountry() {
|
||||||
<div className="team__mobile-team-name">
|
<div className="team__mobile-team-name">
|
||||||
{team.name}
|
{team.name}
|
||||||
<TwitterLink />
|
<TwitterLink />
|
||||||
|
<BskyLink />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -174,6 +177,23 @@ function TwitterLink({ testId }: { testId?: string }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BskyLink() {
|
||||||
|
const { team } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
if (!team.bsky) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className="team__bsky-link"
|
||||||
|
href={bskyUrl(team.bsky)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<BskyIcon />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ActionButtons() {
|
function ActionButtons() {
|
||||||
const { t } = useTranslation(["team"]);
|
const { t } = useTranslation(["team"]);
|
||||||
const user = useUser();
|
const user = useUser();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export const TEAM = {
|
||||||
NAME_MIN_LENGTH: 2,
|
NAME_MIN_LENGTH: 2,
|
||||||
BIO_MAX_LENGTH: 2000,
|
BIO_MAX_LENGTH: 2000,
|
||||||
TWITTER_MAX_LENGTH: 50,
|
TWITTER_MAX_LENGTH: 50,
|
||||||
|
BSKY_MAX_LENGTH: 50,
|
||||||
MAX_MEMBER_COUNT: 10,
|
MAX_MEMBER_COUNT: 10,
|
||||||
MAX_TEAM_COUNT_NON_PATRON: 2,
|
MAX_TEAM_COUNT_NON_PATRON: 2,
|
||||||
MAX_TEAM_COUNT_PATRON: 5,
|
MAX_TEAM_COUNT_PATRON: 5,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ export const editTeamSchema = z.union([
|
||||||
falsyToNull,
|
falsyToNull,
|
||||||
z.string().max(TEAM.TWITTER_MAX_LENGTH).nullable(),
|
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()),
|
css: z.preprocess(falsyToNull, z.string().refine(jsonParseable).nullable()),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,26 @@
|
||||||
fill: #1da1f2;
|
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 {
|
.team__banner__avatar {
|
||||||
grid-area: avatar;
|
grid-area: avatar;
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { BskyIcon } from "~/components/icons/Bsky";
|
||||||
import { LinkIcon } from "~/components/icons/Link";
|
import { LinkIcon } from "~/components/icons/Link";
|
||||||
import { TwitchIcon } from "~/components/icons/Twitch";
|
import { TwitchIcon } from "~/components/icons/Twitch";
|
||||||
import { TwitterIcon } from "~/components/icons/Twitter";
|
import { TwitterIcon } from "~/components/icons/Twitter";
|
||||||
|
|
@ -24,6 +25,7 @@ function SocialLink({ url }: { url: string }) {
|
||||||
youtube: type === "youtube",
|
youtube: type === "youtube",
|
||||||
twitter: type === "twitter",
|
twitter: type === "twitter",
|
||||||
twitch: type === "twitch",
|
twitch: type === "twitch",
|
||||||
|
bsky: type === "bsky",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<SocialLinkIcon url={url} />
|
<SocialLinkIcon url={url} />
|
||||||
|
|
@ -48,6 +50,10 @@ function SocialLinkIcon({ url }: { url: string }) {
|
||||||
return <YouTubeIcon />;
|
return <YouTubeIcon />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "bsky") {
|
||||||
|
return <BskyIcon />;
|
||||||
|
}
|
||||||
|
|
||||||
return <LinkIcon />;
|
return <LinkIcon />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,5 +70,9 @@ const urlToLinkType = (url: string) => {
|
||||||
return "youtube";
|
return "youtube";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url.includes("bsky.app")) {
|
||||||
|
return "bsky";
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -181,3 +181,7 @@
|
||||||
.org__social-link__icon-container.twitter svg {
|
.org__social-link__icon-container.twitter svg {
|
||||||
fill: #1da1f2;
|
fill: #1da1f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.org__social-link__icon-container.bsky path {
|
||||||
|
fill: #1285fe;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ export async function findProfileByIdentifier(
|
||||||
"User.twitter",
|
"User.twitter",
|
||||||
"User.youtubeId",
|
"User.youtubeId",
|
||||||
"User.battlefy",
|
"User.battlefy",
|
||||||
|
"User.bsky",
|
||||||
"User.country",
|
"User.country",
|
||||||
"User.bio",
|
"User.bio",
|
||||||
"User.motionSens",
|
"User.motionSens",
|
||||||
|
|
@ -586,6 +587,7 @@ type UpdateProfileArgs = Pick<
|
||||||
| "stickSens"
|
| "stickSens"
|
||||||
| "inGameName"
|
| "inGameName"
|
||||||
| "battlefy"
|
| "battlefy"
|
||||||
|
| "bsky"
|
||||||
| "css"
|
| "css"
|
||||||
| "favoriteBadgeId"
|
| "favoriteBadgeId"
|
||||||
| "showDiscordUniqueName"
|
| "showDiscordUniqueName"
|
||||||
|
|
@ -628,6 +630,7 @@ export function updateProfile(args: UpdateProfileArgs) {
|
||||||
inGameName: args.inGameName,
|
inGameName: args.inGameName,
|
||||||
css: args.css,
|
css: args.css,
|
||||||
battlefy: args.battlefy,
|
battlefy: args.battlefy,
|
||||||
|
bsky: args.bsky,
|
||||||
favoriteBadgeId: args.favoriteBadgeId,
|
favoriteBadgeId: args.favoriteBadgeId,
|
||||||
showDiscordUniqueName: args.showDiscordUniqueName,
|
showDiscordUniqueName: args.showDiscordUniqueName,
|
||||||
commissionText: args.commissionText,
|
commissionText: args.commissionText,
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,10 @@ const userEditActionSchema = z
|
||||||
falsyToNull,
|
falsyToNull,
|
||||||
z.string().max(USER.BATTLEFY_MAX_LENGTH).nullable(),
|
z.string().max(USER.BATTLEFY_MAX_LENGTH).nullable(),
|
||||||
),
|
),
|
||||||
|
bsky: z.preprocess(
|
||||||
|
falsyToNull,
|
||||||
|
z.string().max(USER.BSKY_MAX_LENGTH).nullable(),
|
||||||
|
),
|
||||||
stickSens: z.preprocess(
|
stickSens: z.preprocess(
|
||||||
processMany(actualNumber, undefinedToNull),
|
processMany(actualNumber, undefinedToNull),
|
||||||
z
|
z
|
||||||
|
|
@ -257,6 +261,7 @@ export default function UserEditPage() {
|
||||||
<InGameNameInputs />
|
<InGameNameInputs />
|
||||||
<SensSelects />
|
<SensSelects />
|
||||||
<BattlefyInput />
|
<BattlefyInput />
|
||||||
|
<BskyInput />
|
||||||
<CountrySelect />
|
<CountrySelect />
|
||||||
<FavBadgeSelect />
|
<FavBadgeSelect />
|
||||||
<WeaponPoolSelect />
|
<WeaponPoolSelect />
|
||||||
|
|
@ -455,6 +460,24 @@ 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() {
|
function WeaponPoolSelect() {
|
||||||
const data = useLoaderData<typeof loader>();
|
const data = useLoaderData<typeof loader>();
|
||||||
const [weapons, setWeapons] = React.useState(data.user.weapons);
|
const [weapons, setWeapons] = React.useState(data.user.weapons);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import { useTranslation } from "react-i18next";
|
||||||
import { Avatar } from "~/components/Avatar";
|
import { Avatar } from "~/components/Avatar";
|
||||||
import { Flag } from "~/components/Flag";
|
import { Flag } from "~/components/Flag";
|
||||||
import { Image, WeaponImage } from "~/components/Image";
|
import { Image, WeaponImage } from "~/components/Image";
|
||||||
|
import { Popover } from "~/components/Popover";
|
||||||
import { BattlefyIcon } from "~/components/icons/Battlefy";
|
import { BattlefyIcon } from "~/components/icons/Battlefy";
|
||||||
|
import { BskyIcon } from "~/components/icons/Bsky";
|
||||||
import { DiscordIcon } from "~/components/icons/Discord";
|
import { DiscordIcon } from "~/components/icons/Discord";
|
||||||
import { TwitchIcon } from "~/components/icons/Twitch";
|
import { TwitchIcon } from "~/components/icons/Twitch";
|
||||||
import { TwitterIcon } from "~/components/icons/Twitter";
|
import { TwitterIcon } from "~/components/icons/Twitter";
|
||||||
|
|
@ -17,6 +19,7 @@ import type { SendouRouteHandle } from "~/utils/remix";
|
||||||
import { rawSensToString } from "~/utils/strings";
|
import { rawSensToString } from "~/utils/strings";
|
||||||
import { assertUnreachable } from "~/utils/types";
|
import { assertUnreachable } from "~/utils/types";
|
||||||
import {
|
import {
|
||||||
|
bskyUrl,
|
||||||
modeImageUrl,
|
modeImageUrl,
|
||||||
navIconUrl,
|
navIconUrl,
|
||||||
teamPage,
|
teamPage,
|
||||||
|
|
@ -25,7 +28,6 @@ import {
|
||||||
} from "~/utils/urls";
|
} from "~/utils/urls";
|
||||||
import type { UserPageLoaderData } from "./u.$identifier";
|
import type { UserPageLoaderData } from "./u.$identifier";
|
||||||
|
|
||||||
import { Popover } from "~/components/Popover";
|
|
||||||
import { loader } from "../loaders/u.$identifier.index.server";
|
import { loader } from "../loaders/u.$identifier.index.server";
|
||||||
export { loader };
|
export { loader };
|
||||||
|
|
||||||
|
|
@ -67,6 +69,9 @@ export default function UserInfoPage() {
|
||||||
{data.user.battlefy ? (
|
{data.user.battlefy ? (
|
||||||
<SocialLink type="battlefy" identifier={data.user.battlefy} />
|
<SocialLink type="battlefy" identifier={data.user.battlefy} />
|
||||||
) : null}
|
) : null}
|
||||||
|
{data.user.bsky ? (
|
||||||
|
<SocialLink type="bsky" identifier={data.user.bsky} />
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<BannedInfo />
|
<BannedInfo />
|
||||||
|
|
@ -168,7 +173,7 @@ function SecondaryTeamsPopover() {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SocialLinkProps {
|
interface SocialLinkProps {
|
||||||
type: "youtube" | "twitter" | "twitch" | "battlefy";
|
type: "youtube" | "twitter" | "twitch" | "battlefy" | "bsky";
|
||||||
identifier: string;
|
identifier: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,7 +181,7 @@ export function SocialLink({
|
||||||
type,
|
type,
|
||||||
identifier,
|
identifier,
|
||||||
}: {
|
}: {
|
||||||
type: "youtube" | "twitter" | "twitch" | "battlefy";
|
type: SocialLinkProps["type"];
|
||||||
identifier: string;
|
identifier: string;
|
||||||
}) {
|
}) {
|
||||||
const href = () => {
|
const href = () => {
|
||||||
|
|
@ -189,6 +194,8 @@ export function SocialLink({
|
||||||
return `https://www.youtube.com/channel/${identifier}`;
|
return `https://www.youtube.com/channel/${identifier}`;
|
||||||
case "battlefy":
|
case "battlefy":
|
||||||
return `https://battlefy.com/users/${identifier}`;
|
return `https://battlefy.com/users/${identifier}`;
|
||||||
|
case "bsky":
|
||||||
|
return bskyUrl(identifier);
|
||||||
default:
|
default:
|
||||||
assertUnreachable(type);
|
assertUnreachable(type);
|
||||||
}
|
}
|
||||||
|
|
@ -201,6 +208,7 @@ export function SocialLink({
|
||||||
twitter: type === "twitter",
|
twitter: type === "twitter",
|
||||||
twitch: type === "twitch",
|
twitch: type === "twitch",
|
||||||
battlefy: type === "battlefy",
|
battlefy: type === "battlefy",
|
||||||
|
bsky: type === "bsky",
|
||||||
})}
|
})}
|
||||||
href={href()}
|
href={href()}
|
||||||
>
|
>
|
||||||
|
|
@ -219,6 +227,8 @@ function SocialLinkIcon({ type }: Pick<SocialLinkProps, "type">) {
|
||||||
return <YouTubeIcon />;
|
return <YouTubeIcon />;
|
||||||
case "battlefy":
|
case "battlefy":
|
||||||
return <BattlefyIcon />;
|
return <BattlefyIcon />;
|
||||||
|
case "bsky":
|
||||||
|
return <BskyIcon />;
|
||||||
default:
|
default:
|
||||||
assertUnreachable(type);
|
assertUnreachable(type);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,17 @@
|
||||||
fill: #de4c5e;
|
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 {
|
.u__extra-infos {
|
||||||
display: flex;
|
display: flex;
|
||||||
max-width: 24rem;
|
max-width: 24rem;
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ export const SPR_INFO_URL =
|
||||||
|
|
||||||
export const twitterUrl = (accountName: string) =>
|
export const twitterUrl = (accountName: string) =>
|
||||||
`https://twitter.com/${accountName}`;
|
`https://twitter.com/${accountName}`;
|
||||||
|
export const bskyUrl = (accountName: string) =>
|
||||||
|
`https://bsky.app/profile/${accountName}`;
|
||||||
export const twitchUrl = (accountName: string) =>
|
export const twitchUrl = (accountName: string) =>
|
||||||
`https://twitch.tv/${accountName}`;
|
`https://twitch.tv/${accountName}`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
"roles.COACH": "Coach",
|
"roles.COACH": "Coach",
|
||||||
"roles.CHEERLEADER": "Cheerleader",
|
"roles.CHEERLEADER": "Cheerleader",
|
||||||
"forms.fields.teamTwitter": "Team Twitter",
|
"forms.fields.teamTwitter": "Team Twitter",
|
||||||
|
"forms.fields.teamBsky": "Team Bluesky",
|
||||||
"forms.fields.bio": "Bio",
|
"forms.fields.bio": "Bio",
|
||||||
"forms.fields.uploadImages": "Upload images",
|
"forms.fields.uploadImages": "Upload images",
|
||||||
"forms.fields.uploadImages.pfp": "Profile Picture",
|
"forms.fields.uploadImages.pfp": "Profile Picture",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"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, Twitter and Twitch accounts come from your Discord account. See <1>FAQ</1> for more information.",
|
||||||
"favoriteBadge": "Favorite Badge",
|
"favoriteBadge": "Favorite Badge",
|
||||||
"battlefy": "Battlefy account name",
|
"battlefy": "Battlefy account name",
|
||||||
|
"bsky": "Bluesky account name",
|
||||||
|
|
||||||
"forms.showDiscordUniqueName": "Show Discord username",
|
"forms.showDiscordUniqueName": "Show Discord username",
|
||||||
"forms.showDiscordUniqueName.info": "Show your unique Discord name ({{discordUniqueName}}) publicly?",
|
"forms.showDiscordUniqueName.info": "Show your unique Discord name ({{discordUniqueName}}) publicly?",
|
||||||
|
|
|
||||||
7
migrations/070-bsky.js
Normal file
7
migrations/070-bsky.js
Normal file
|
|
@ -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();
|
||||||
|
})();
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user