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:
Kalle 2024-09-28 10:43:49 +03:00 committed by GitHub
parent 803b93e061
commit 59d77642e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 198 additions and 60 deletions

View 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>
);
}

View File

@ -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,

View File

@ -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;

View File

@ -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,

View File

@ -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;
}

View File

@ -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>();

View File

@ -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();

View File

@ -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,

View File

@ -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()),
}), }),
]); ]);

View File

@ -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;

View File

@ -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;
}; };

View File

@ -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;
}

View File

@ -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,

View File

@ -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);

View File

@ -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);
} }

View File

@ -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;

View File

@ -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}`;

View File

@ -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",

View File

@ -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
View 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();
})();
}