diff --git a/app/db/tables.ts b/app/db/tables.ts index 98df43a80..86d767590 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -45,6 +45,8 @@ export interface Team { inviteCode: string; name: string; bsky: string | null; + /** Team's tag, typically used in-game in front of users' names to indicate they are a member of the team. */ + tag: string | null; } export interface TeamMember { diff --git a/app/features/team/TeamRepository.server.ts b/app/features/team/TeamRepository.server.ts index 3f775c3f5..642ef61db 100644 --- a/app/features/team/TeamRepository.server.ts +++ b/app/features/team/TeamRepository.server.ts @@ -20,6 +20,7 @@ export function findAllUndisbanded() { .select(({ eb }) => [ "Team.customUrl", "Team.name", + "Team.tag", concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as( "avatarUrl", ), @@ -75,6 +76,7 @@ export function findByCustomUrl( "Team.name", "Team.bsky", "Team.bio", + "Team.tag", "Team.customUrl", "Team.css", concatUserSubmittedImagePrefix(eb.ref("AvatarImage.url")).as("avatarUrl"), @@ -277,10 +279,11 @@ export async function update({ customUrl, bio, bsky, + tag, css, }: Pick< Insertable, - "id" | "name" | "customUrl" | "bio" | "bsky" + "id" | "name" | "customUrl" | "bio" | "bsky" | "tag" > & { css: string | null }) { return db .updateTable("AllTeam") @@ -289,6 +292,7 @@ export async function update({ customUrl, bio, bsky, + tag, css, }) .where("id", "=", id) diff --git a/app/features/team/actions/t.$customUrl.edit.server.test.ts b/app/features/team/actions/t.$customUrl.edit.server.test.ts index cbc6a31d0..35b47e027 100644 --- a/app/features/team/actions/t.$customUrl.edit.server.test.ts +++ b/app/features/team/actions/t.$customUrl.edit.server.test.ts @@ -22,6 +22,7 @@ const DEFAULT_FIELDS = { name: "Team 1", bio: "", bsky: "", + tag: "", } as const; describe("team page editing", () => { diff --git a/app/features/team/routes/t.$customUrl.edit.tsx b/app/features/team/routes/t.$customUrl.edit.tsx index 5518e7de1..5767bc7d6 100644 --- a/app/features/team/routes/t.$customUrl.edit.tsx +++ b/app/features/team/routes/t.$customUrl.edit.tsx @@ -59,6 +59,7 @@ export default function EditTeamPage() { ) : null} + (); + const [value, setValue] = React.useState(team.tag ?? ""); + + return ( +
+ + setValue(e.target.value)} + /> + {t("team:forms.info.tag")} +
+ ); +} + function BlueskyInput() { 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 9695f6077..ce4901834 100644 --- a/app/features/team/routes/t.$customUrl.tsx +++ b/app/features/team/routes/t.$customUrl.tsx @@ -98,6 +98,11 @@ function TeamBanner() { })}
+ {team.tag ? ( +
+ {team.tag} +
+ ) : null} {team.name}
@@ -124,6 +129,11 @@ function MobileTeamNameCountry() { {team.name} + {team.tag ? ( +
+ {team.tag} +
+ ) : null} ); } diff --git a/app/features/team/routes/t.tsx b/app/features/team/routes/t.tsx index f5e2e1b3b..5567c8097 100644 --- a/app/features/team/routes/t.tsx +++ b/app/features/team/routes/t.tsx @@ -54,22 +54,33 @@ export default function TeamSearchPage() { const [inputValue, setInputValue] = React.useState(""); const data = useLoaderData(); - const filteredTeams = data.teams.filter((team) => { - if (!inputValue) return true; + const filteredTeams = () => { + if (!inputValue) return data.teams; const lowerCaseInput = inputValue.toLowerCase(); + const matchingTeams = data.teams.filter((team) => { + if (team.name.toLowerCase().includes(lowerCaseInput)) return true; + if (team.tag && team.tag.toLowerCase() === lowerCaseInput) return true; + if ( + team.members.some((m) => + m.username.toLowerCase().includes(lowerCaseInput), + ) + ) { + return true; + } - if (team.name.toLowerCase().includes(lowerCaseInput)) return true; - if ( - team.members.some((m) => - m.username.toLowerCase().includes(lowerCaseInput), - ) - ) { - return true; - } + return false; + }); - return false; - }); + return matchingTeams.sort((a, b) => { + const aTagExactMatch = a.tag && a.tag.toLowerCase() === lowerCaseInput; + const bTagExactMatch = b.tag && b.tag.toLowerCase() === lowerCaseInput; + + if (aTagExactMatch && !bTagExactMatch) return -1; + if (!aTagExactMatch && bTagExactMatch) return 1; + return 0; + }); + }; const { itemsToDisplay, @@ -80,7 +91,7 @@ export default function TeamSearchPage() { previousPage, setPage, } = usePagination({ - items: filteredTeams, + items: filteredTeams(), pageSize: TEAMS_PER_PAGE, }); @@ -125,6 +136,9 @@ export default function TeamSearchPage() { data-testid={`team-${i}`} > {team.name} + {team.tag ? ( + {team.tag} + ) : null}
{team.members.length === 1 diff --git a/app/features/team/team-constants.ts b/app/features/team/team-constants.ts index 3f462d72b..db09a651d 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, BSKY_MAX_LENGTH: 50, + TAG_MAX_LENGTH: 6, 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 b96c4b2dc..1bba54aa5 100644 --- a/app/features/team/team-schemas.server.ts +++ b/app/features/team/team-schemas.server.ts @@ -47,6 +47,10 @@ export const editTeamSchema = z.union([ falsyToNull, z.string().max(TEAM.BSKY_MAX_LENGTH).nullable(), ), + tag: z.preprocess( + falsyToNull, + z.string().max(TEAM.TAG_MAX_LENGTH).nullable(), + ), css: customCssVarObject, }), ]); diff --git a/app/features/team/team.css b/app/features/team/team.css index 058c31c5f..6ab233c84 100644 --- a/app/features/team/team.css +++ b/app/features/team/team.css @@ -28,6 +28,15 @@ color: var(--text-lighter); } +.team-search__team__tag { + font-size: var(--fonts-xs); + color: var(--theme); + padding: var(--s-0-5) var(--s-1); + border-radius: var(--rounded-xs); + margin-left: var(--s-2); + font-weight: var(--bold); +} + .team-search__team__avatar-placeholder { height: 64px; min-width: 64px; @@ -78,6 +87,7 @@ display: none; align-items: center; gap: var(--s-3); + position: relative; } .team__bsky-link { @@ -134,6 +144,27 @@ gap: var(--s-2); } +.team__banner__tag { + font-size: var(--fonts-sm); + background-color: var(--theme-transparent); + color: var(--theme); + padding: var(--s-1) var(--s-1-5); + border-radius: var(--rounded); +} + +.team__banner__tag__desktop { + position: absolute; + bottom: 41px; + right: 0; + display: none; +} + +.team__banner__tag__mobile { + font-size: var(--fonts-xs); + padding: var(--s-0-5) var(--s-1); + margin-block: var(--s-1); +} + .team__banner__avatar img { border-radius: 100%; } @@ -301,6 +332,9 @@ .team__banner__name { display: flex; } + .team__banner__tag__desktop { + display: initial; + } .team__banner__avatar > div { width: 10rem; diff --git a/db-test.sqlite3 b/db-test.sqlite3 index 94db17c5a..4ff8d9d88 100644 Binary files a/db-test.sqlite3 and b/db-test.sqlite3 differ diff --git a/e2e/team.spec.ts b/e2e/team.spec.ts index 7f55d22b3..1cafe2292 100644 --- a/e2e/team.spec.ts +++ b/e2e/team.spec.ts @@ -50,6 +50,25 @@ test.describe("Team search page", () => { await expect(page).toHaveURL(/chimera/); }); + + test("filters teams by tag & displays tag", async ({ page }) => { + await seed(page); + await impersonate(page, ADMIN_ID); + await navigate({ page, url: teamPage("alliance-rogue") }); + + await page.getByTestId("edit-team-button").click(); + await page.getByLabel("Tag").fill("AR"); + await page.getByTestId("edit-team-submit-button").click(); + + await navigate({ page, url: TEAM_SEARCH_PAGE }); + + const searchInput = page.getByTestId("team-search-input"); + await searchInput.fill("ar"); + + const firstTeamName = page.getByTestId("team-0"); + await expect(firstTeamName).toContainText("Alliance Rogue"); + await expect(firstTeamName).toContainText("AR"); + }); }); test.describe("Team page", () => { diff --git a/locales/da/team.json b/locales/da/team.json index 4354d1ded..645841b60 100644 --- a/locales/da/team.json +++ b/locales/da/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "", "forms.fields.teamBsky": "", "forms.fields.bio": "Bio", + "forms.fields.tag": "", "forms.fields.uploadImages": "Upload billeder", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "Profilbillede", "forms.fields.uploadImages.banner": "Holdbanner", "forms.info.name": "Note: Hvis du ændrer dit holds navn, så kan andre hold overtage det tidligere holdnavn og URL-adresse.", + "forms.info.tag": "", "forms.errors.duplicateName": "Holdnavnet er taget af et andet hold", "roster.teamFull": "Holdet kan ikke få flere medlemmer", "roster.inviteLink.header": "Del invitationslinket for at tilføje medlemmer", diff --git a/locales/de/team.json b/locales/de/team.json index bd71ef2c6..ac421f7fe 100644 --- a/locales/de/team.json +++ b/locales/de/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "", "forms.fields.teamBsky": "", "forms.fields.bio": "Bio", + "forms.fields.tag": "", "forms.fields.uploadImages": "Bilder hochladen", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "Profilbild", "forms.fields.uploadImages.banner": "Teambild-Banner", "forms.info.name": "Hinweis: Wenn du den Namen deines Teams änderst, können andere Teams den Namen und und die URL für sich beanspruchen.", + "forms.info.tag": "", "forms.errors.duplicateName": "Es gibt bereits ein Team mit diesem Namen", "roster.teamFull": "Team ist voll", "roster.inviteLink.header": "Teile den Einladungslink, um Mitglieder hinzuzufügen", diff --git a/locales/en/team.json b/locales/en/team.json index 1615ed59f..047da4bbb 100644 --- a/locales/en/team.json +++ b/locales/en/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "Cheerleader", "forms.fields.teamBsky": "Team Bluesky", "forms.fields.bio": "Bio", + "forms.fields.tag": "Tag", "forms.fields.uploadImages": "Upload images", "forms.fields.removeImages": "Remove images", "forms.fields.uploadImages.pfp": "Profile Picture", "forms.fields.uploadImages.banner": "Team Picture Banner", "forms.info.name": "Note that if you change your team's name then someone else can claim the name and URL for their team", + "forms.info.tag": "Typically used before in-game name to indicate membership of a team (e.g. [TAG] PlayerName)", "forms.errors.duplicateName": "There is already a team with this name", "roster.teamFull": "Team is full", "roster.inviteLink.header": "Share invite link to add members", diff --git a/locales/es-ES/team.json b/locales/es-ES/team.json index a0aae4552..c2f74e077 100644 --- a/locales/es-ES/team.json +++ b/locales/es-ES/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "", "forms.fields.teamBsky": "", "forms.fields.bio": "Bio", + "forms.fields.tag": "", "forms.fields.uploadImages": "Subir imágenes", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "Imagen de perfil", "forms.fields.uploadImages.banner": "Banner del equipo", "forms.info.name": "Nota que si cambias el nombre de tu equipo, el nombre y la URL estarán libres para que otro equipo los tome", + "forms.info.tag": "", "forms.errors.duplicateName": "Ya existe un equipo con ese nombre", "roster.teamFull": "El equipo esta lleno", "roster.inviteLink.header": "Comparte el link de invitación para agregar más miembros", diff --git a/locales/es-US/team.json b/locales/es-US/team.json index 8fdc5d85d..851f48d66 100644 --- a/locales/es-US/team.json +++ b/locales/es-US/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "", "forms.fields.teamBsky": "", "forms.fields.bio": "Bio", + "forms.fields.tag": "", "forms.fields.uploadImages": "Subir imágenes", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "Imagen de perfil", "forms.fields.uploadImages.banner": "Banner del equipo", "forms.info.name": "Nota que si cambias el nombre de tu equipo, el nombre y la URL estarán libres para que otro equipo los tome", + "forms.info.tag": "", "forms.errors.duplicateName": "Ya existe un equipo con ese nombre", "roster.teamFull": "El equipo esta lleno", "roster.inviteLink.header": "Comparte el link de invitación para agregar más miembros", diff --git a/locales/fr-CA/team.json b/locales/fr-CA/team.json index 02693f00b..3006937bd 100644 --- a/locales/fr-CA/team.json +++ b/locales/fr-CA/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "", "forms.fields.teamBsky": "", "forms.fields.bio": "Bio", + "forms.fields.tag": "", "forms.fields.uploadImages": "Soumettre images", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "Emblème", "forms.fields.uploadImages.banner": "Bannière d'équipe", "forms.info.name": "Veuillez noter que si vous changer le nom de l'équipe, quelqu'un d'autre pourra s'emparer de l'ancien nom et URL", + "forms.info.tag": "", "forms.errors.duplicateName": "Il y a déjà une équipe avec ce nom", "roster.teamFull": "L'équipe est complète", "roster.inviteLink.header": "Partager le lien d'invitation pour ajouter des membres", diff --git a/locales/fr-EU/team.json b/locales/fr-EU/team.json index afd756f92..fc49b7556 100644 --- a/locales/fr-EU/team.json +++ b/locales/fr-EU/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "Cheerleader", "forms.fields.teamBsky": "Team Bluesky", "forms.fields.bio": "Bio", + "forms.fields.tag": "", "forms.fields.uploadImages": "Soumettre images", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "Emblème", "forms.fields.uploadImages.banner": "Bannière d'équipe", "forms.info.name": "Veuillez noter que si vous changer le nom de l'équipe, quelqu'un d'autre pourra s'emparer de l'ancien nom et URL", + "forms.info.tag": "", "forms.errors.duplicateName": "Il y a déjà une équipe avec ce nom", "roster.teamFull": "L'équipe est complète", "roster.inviteLink.header": "Partager le lien d'invitation pour ajouter des membres", diff --git a/locales/he/team.json b/locales/he/team.json index ca252c1cd..0c06ea5e8 100644 --- a/locales/he/team.json +++ b/locales/he/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "", "forms.fields.teamBsky": "", "forms.fields.bio": "ביו", + "forms.fields.tag": "", "forms.fields.uploadImages": "העלאת תמונות", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "תמונת פרופיל", "forms.fields.uploadImages.banner": "תמונת באנר של הצוות", "forms.info.name": "שימו לב שאם תשנו את שם הצוות שלכם, מישהו אחר יוכל לקחת בעלות על השם ועל כתובת האתר עבור הצוות שלו", + "forms.info.tag": "", "forms.errors.duplicateName": "יש כבר צוות בשם הזה", "roster.teamFull": "הצוות מלא", "roster.inviteLink.header": "שיתוף קישור הזמנה להוספת חברי צוות", diff --git a/locales/it/team.json b/locales/it/team.json index ec037dc6f..f4a1accb2 100644 --- a/locales/it/team.json +++ b/locales/it/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "Cheerleader", "forms.fields.teamBsky": "Bluesky del team", "forms.fields.bio": "Bio", + "forms.fields.tag": "", "forms.fields.uploadImages": "Carica immagini", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "Foto profilo", "forms.fields.uploadImages.banner": "Foto banner del team", "forms.info.name": "Nota che se cambi il nome del team, qualcun altro può assumere nome e URL per il proprio team", + "forms.info.tag": "", "forms.errors.duplicateName": "Esiste già un team con questo nome", "roster.teamFull": "Il team è completo", "roster.inviteLink.header": "Condividi link di invito per aggiungere membri", diff --git a/locales/ja/team.json b/locales/ja/team.json index 96fa6f87c..69869b8f5 100644 --- a/locales/ja/team.json +++ b/locales/ja/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "引き立て役(チアリーダー)", "forms.fields.teamBsky": "チームの Bluesky", "forms.fields.bio": "Bio", + "forms.fields.tag": "", "forms.fields.uploadImages": "画像をアップロード", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "プロファイル画像", "forms.fields.uploadImages.banner": "チーム画像バナー", "forms.info.name": "注意: チーム名を変更した場合、他のプレイヤーが変更前の名前と URL を別のチームのために使用することができるようになります。", + "forms.info.tag": "", "forms.errors.duplicateName": "そのチーム名はすでに使用されています", "roster.teamFull": "チームは満員です", "roster.inviteLink.header": "招待リンクを共有する", diff --git a/locales/ko/team.json b/locales/ko/team.json index 80025af73..690c4ede8 100644 --- a/locales/ko/team.json +++ b/locales/ko/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "", "forms.fields.teamBsky": "", "forms.fields.bio": "", + "forms.fields.tag": "", "forms.fields.uploadImages": "", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "", "forms.fields.uploadImages.banner": "", "forms.info.name": "", + "forms.info.tag": "", "forms.errors.duplicateName": "", "roster.teamFull": "", "roster.inviteLink.header": "", diff --git a/locales/nl/team.json b/locales/nl/team.json index 80025af73..690c4ede8 100644 --- a/locales/nl/team.json +++ b/locales/nl/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "", "forms.fields.teamBsky": "", "forms.fields.bio": "", + "forms.fields.tag": "", "forms.fields.uploadImages": "", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "", "forms.fields.uploadImages.banner": "", "forms.info.name": "", + "forms.info.tag": "", "forms.errors.duplicateName": "", "roster.teamFull": "", "roster.inviteLink.header": "", diff --git a/locales/pl/team.json b/locales/pl/team.json index 884275ae2..9d53c821d 100644 --- a/locales/pl/team.json +++ b/locales/pl/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "", "forms.fields.teamBsky": "", "forms.fields.bio": "Opis", + "forms.fields.tag": "", "forms.fields.uploadImages": "Wstaw zdjęcia", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "Zdjęcie profilowe", "forms.fields.uploadImages.banner": "Grafika drużyny", "forms.info.name": "Uwaga: Jeśli zmienisz imię drużyny, ktoś inny może użyć twoje stare imię i URL", + "forms.info.tag": "", "forms.errors.duplicateName": "Istnieje już drużyna o tym imieniu", "roster.teamFull": "Drużyna jest pełna", "roster.inviteLink.header": "Udostępnij link by dodać członków", diff --git a/locales/pt-BR/team.json b/locales/pt-BR/team.json index 46907a47b..197736ff8 100644 --- a/locales/pt-BR/team.json +++ b/locales/pt-BR/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "", "forms.fields.teamBsky": "", "forms.fields.bio": "Bio", + "forms.fields.tag": "", "forms.fields.uploadImages": "Fazer upload de imagens", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "Imagem do perfil", "forms.fields.uploadImages.banner": "Capa do perfil", "forms.info.name": "Lembre-se que se você mudar o nome do seu time, alguém pode resgatar o nome e o URL para o time dele(a)", + "forms.info.tag": "", "forms.errors.duplicateName": "Já existe um time com esse nome", "roster.teamFull": "O time está cheio", "roster.inviteLink.header": "Compartilhe o link de convite para adicionar membros", diff --git a/locales/ru/team.json b/locales/ru/team.json index 1b2d088ac..87b7440f6 100644 --- a/locales/ru/team.json +++ b/locales/ru/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "Чирлидер", "forms.fields.teamBsky": "Bluesky команды", "forms.fields.bio": "Описание", + "forms.fields.tag": "", "forms.fields.uploadImages": "Загрузить изображения", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "Аватар команды", "forms.fields.uploadImages.banner": "Баннер команды", "forms.info.name": "Обратите внимание, что если вы измените название команды, то кто-то другой может забрать себе URL и название для своей команды", + "forms.info.tag": "", "forms.errors.duplicateName": "Уже существует команда с таким названием", "roster.teamFull": "Команда заполнена", "roster.inviteLink.header": "Поделитесь ссылкой на приглашение, чтобы добавить участников", diff --git a/locales/zh/team.json b/locales/zh/team.json index 72faa35a2..ae0e7f874 100644 --- a/locales/zh/team.json +++ b/locales/zh/team.json @@ -33,11 +33,13 @@ "roles.CHEERLEADER": "", "forms.fields.teamBsky": "", "forms.fields.bio": "简介", + "forms.fields.tag": "", "forms.fields.uploadImages": "上传图片", "forms.fields.removeImages": "", "forms.fields.uploadImages.pfp": "头像", "forms.fields.uploadImages.banner": "队伍横幅", "forms.info.name": "请注意,如果您更改了队名,那么其他人便可以使用之前的队名和URL了。", + "forms.info.tag": "", "forms.errors.duplicateName": "该队名已被使用", "roster.teamFull": "成员已满", "roster.inviteLink.header": "分享邀请链接来添加成员", diff --git a/migrations/106-team-tag.js b/migrations/106-team-tag.js new file mode 100644 index 000000000..8c061e870 --- /dev/null +++ b/migrations/106-team-tag.js @@ -0,0 +1,5 @@ +export function up(db) { + db.transaction(() => { + db.prepare(/* sql */ `alter table "AllTeam" add "tag" text`).run(); + })(); +}