From 59d77642e5dd74961fb230246c9c1dd5a3c0ae9c Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 28 Sep 2024 10:43:49 +0300 Subject: [PATCH] Bluesky for user & team pages (#1891) * Migrations * Bluesky for team page * Unify interfaces * For user page * To org social links --- app/components/icons/Bsky.tsx | 16 ++++++++++ app/constants.ts | 1 + app/db/tables.ts | 32 ++++++------------- app/features/team/TeamRepository.server.ts | 28 ++++++++++++++++ app/features/team/queries/edit.server.ts | 32 ------------------- .../team/routes/t.$customUrl.edit.tsx | 24 ++++++++++++-- app/features/team/routes/t.$customUrl.tsx | 22 ++++++++++++- app/features/team/team-constants.ts | 1 + app/features/team/team-schemas.server.ts | 4 +++ app/features/team/team.css | 20 ++++++++++++ .../components/SocialLinksList.tsx | 10 ++++++ .../tournament-organization.css | 4 +++ .../user-page/UserRepository.server.ts | 3 ++ .../user-page/routes/u.$identifier.edit.tsx | 23 +++++++++++++ .../user-page/routes/u.$identifier.index.tsx | 16 ++++++++-- app/styles/u.css | 11 +++++++ app/utils/urls.ts | 2 ++ locales/en/team.json | 1 + locales/en/user.json | 1 + migrations/070-bsky.js | 7 ++++ 20 files changed, 198 insertions(+), 60 deletions(-) create mode 100644 app/components/icons/Bsky.tsx delete mode 100644 app/features/team/queries/edit.server.ts create mode 100644 migrations/070-bsky.js diff --git a/app/components/icons/Bsky.tsx b/app/components/icons/Bsky.tsx new file mode 100644 index 0000000000..9f9c07c490 --- /dev/null +++ b/app/components/icons/Bsky.tsx @@ -0,0 +1,16 @@ +export function BskyIcon({ className }: { className?: string }) { + return ( + + Bluesky butterfly logo + + + ); +} diff --git a/app/constants.ts b/app/constants.ts index 80aeafe0e3..00a80496c9 100644 --- a/app/constants.ts +++ b/app/constants.ts @@ -8,6 +8,7 @@ export const USER = { CUSTOM_URL_MAX_LENGTH: 32, CUSTOM_NAME_MAX_LENGTH: 32, 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, diff --git a/app/db/tables.ts b/app/db/tables.ts index e67a3f2f16..6d213dcace 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -20,7 +20,9 @@ export type Generated = T extends ColumnType ? ColumnType : ColumnType; -export interface AllTeam { +export type MemberRole = (typeof TEAM_MEMBER_ROLES)[number]; + +export interface Team { avatarImgId: number | null; bannerImgId: number | null; bio: string | null; @@ -32,11 +34,10 @@ export interface AllTeam { inviteCode: string; name: string; twitter: string | null; + bsky: string | null; } -export type MemberRole = (typeof TEAM_MEMBER_ROLES)[number]; - -export interface AllTeamMember { +export interface TeamMember { createdAt: Generated; isOwner: Generated; leftAt: number | null; @@ -46,20 +47,6 @@ export interface AllTeamMember { isMainTeam: number; } -export interface Team { - avatarImgId: number | null; - bannerImgId: number | null; - bio: string | null; - createdAt: number | null; - css: ColumnType | null, string | null, string | null>; - customUrl: string; - deletedAt: number | null; - id: GeneratedAlways; - inviteCode: string; - name: string; - twitter: string | null; -} - export interface Art { authorId: number; createdAt: Generated; @@ -759,6 +746,7 @@ export interface User { stickSens: number | null; twitch: string | null; twitter: string | null; + bsky: string | null; battlefy: string | null; vc: Generated<"YES" | "NO" | "LISTEN_ONLY">; youtubeId: string | null; @@ -848,8 +836,8 @@ export type Tables = { [P in keyof DB]: Selectable }; export type TablesInsertable = { [P in keyof DB]: Insertable }; export interface DB { - AllTeam: AllTeam; - AllTeamMember: AllTeamMember; + AllTeam: Team; + AllTeamMember: TeamMember; Art: Art; ArtTag: ArtTag; ArtUserMetadata: ArtUserMetadata; @@ -887,8 +875,8 @@ export interface DB { SplatoonPlayer: SplatoonPlayer; TaggedArt: TaggedArt; Team: Team; - TeamMember: AllTeamMember; - TeamMemberWithSecondary: AllTeamMember; + TeamMember: TeamMember; + TeamMemberWithSecondary: TeamMember; Tournament: Tournament; TournamentStaff: TournamentStaff; TournamentBadgeOwner: TournamentBadgeOwner; diff --git a/app/features/team/TeamRepository.server.ts b/app/features/team/TeamRepository.server.ts index 87d38fe2ae..f7839653da 100644 --- a/app/features/team/TeamRepository.server.ts +++ b/app/features/team/TeamRepository.server.ts @@ -67,6 +67,7 @@ export function findByCustomUrl(customUrl: string) { "Team.id", "Team.name", "Team.twitter", + "Team.bsky", "Team.bio", "Team.customUrl", "Team.css", @@ -140,6 +141,33 @@ export async function create( }); } +export async function update({ + id, + name, + customUrl, + bio, + twitter, + bsky, + css, +}: Pick< + Insertable, + "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({ userId, teamId, diff --git a/app/features/team/queries/edit.server.ts b/app/features/team/queries/edit.server.ts deleted file mode 100644 index 83c291cb27..0000000000 --- a/app/features/team/queries/edit.server.ts +++ /dev/null @@ -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) { - return stm.get({ - id, - name, - customUrl, - bio, - twitter, - css, - }) as Team; -} diff --git a/app/features/team/routes/t.$customUrl.edit.tsx b/app/features/team/routes/t.$customUrl.edit.tsx index aeaaf937a1..b832948d77 100644 --- a/app/features/team/routes/t.$customUrl.edit.tsx +++ b/app/features/team/routes/t.$customUrl.edit.tsx @@ -37,7 +37,6 @@ import { } from "~/utils/urls"; import * as TeamRepository from "../TeamRepository.server"; import { deleteTeam } from "../queries/deleteTeam.server"; -import { edit } from "../queries/edit.server"; import { TEAM } from "../team-constants"; import { editTeamSchema, teamParamsSchema } from "../team-schemas.server"; 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, customUrl: newCustomUrl, ...data, @@ -158,6 +157,7 @@ export default function EditTeamPage() { ) : null} + (); + 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>FAQ 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(); + })(); +}