From a2b3e36c49fbc07f9a4004a5f5282b316a6bbd04 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 1 Nov 2025 11:49:43 +0200 Subject: [PATCH] Established orgs (#2609) --- app/db/seed/index.ts | 5 + app/db/tables.ts | 1 + .../calendar/CalendarRepository.server.ts | 1 + .../calendar/actions/calendar.new.server.ts | 51 ++++++---- .../calendar/loaders/calendar.new.server.ts | 23 ++++- app/features/calendar/routes/calendar.new.tsx | 32 +++--- app/features/info/routes/faq.tsx | 2 +- ...amentOrganizationRepository.server.test.ts | 92 ++++++++++++++++++ ...TournamentOrganizationRepository.server.ts | 39 ++++++-- .../actions/org.$slug.server.ts | 25 ++++- .../routes/org.$slug.tsx | 42 +++++++- .../tournament-organization-schemas.ts | 6 ++ db-test.sqlite3 | Bin 1060864 -> 1064960 bytes docs/tournament-creation.md | 4 + e2e/org.spec.ts | 43 +++++++- locales/da/faq.json | 4 +- locales/de/faq.json | 4 +- locales/en/faq.json | 4 +- locales/es-ES/faq.json | 4 +- locales/es-US/faq.json | 4 +- locales/fr-CA/faq.json | 4 +- locales/fr-EU/faq.json | 4 +- locales/he/faq.json | 4 +- locales/it/faq.json | 4 +- locales/ja/faq.json | 4 +- locales/ko/faq.json | 4 +- locales/nl/faq.json | 4 +- locales/pl/faq.json | 4 +- locales/pt-BR/faq.json | 4 +- locales/ru/faq.json | 4 +- locales/zh/faq.json | 4 +- .../102-tournament-org-is-established.js | 8 ++ 32 files changed, 375 insertions(+), 63 deletions(-) create mode 100644 app/features/tournament-organization/TournamentOrganizationRepository.server.test.ts create mode 100644 migrations/102-tournament-org-is-established.js diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts index 2e1d0e9e6..c9d697fe4 100644 --- a/app/db/seed/index.ts +++ b/app/db/seed/index.ts @@ -2624,6 +2624,11 @@ async function organization() { role: "MEMBER", roleDisplayName: null, }, + { + userId: 3, + role: "ADMIN", + roleDisplayName: null, + }, ], series: [], badges: [], diff --git a/app/db/tables.ts b/app/db/tables.ts index 07bbb2197..1fc41d731 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -716,6 +716,7 @@ export interface TournamentOrganization { description: string | null; socials: JSONColumnTypeNullable; avatarImgId: number | null; + isEstablished: Generated; } export const TOURNAMENT_ORGANIZATION_ROLES = [ diff --git a/app/features/calendar/CalendarRepository.server.ts b/app/features/calendar/CalendarRepository.server.ts index a94af6189..50183971c 100644 --- a/app/features/calendar/CalendarRepository.server.ts +++ b/app/features/calendar/CalendarRepository.server.ts @@ -89,6 +89,7 @@ function tournamentOrganization(organizationId: Expression) { "TournamentOrganization.id", "TournamentOrganization.name", "TournamentOrganization.slug", + "TournamentOrganization.isEstablished", "UserSubmittedImage.url as avatarUrl", ]) .whereRef("TournamentOrganization.id", "=", organizationId), diff --git a/app/features/calendar/actions/calendar.new.server.ts b/app/features/calendar/actions/calendar.new.server.ts index 5fced4a9e..f2be3cac2 100644 --- a/app/features/calendar/actions/calendar.new.server.ts +++ b/app/features/calendar/actions/calendar.new.server.ts @@ -1,10 +1,7 @@ import type { ActionFunction } from "@remix-run/node"; import { redirect } from "@remix-run/node"; import type { CalendarEventTag } from "~/db/tables"; -import { - type AuthenticatedUser, - requireUser, -} from "~/features/auth/core/user.server"; +import { requireUser } from "~/features/auth/core/user.server"; import * as BadgeRepository from "~/features/badges/BadgeRepository.server"; import * as CalendarRepository from "~/features/calendar/CalendarRepository.server"; import { newCalendarEventActionSchema } from "~/features/calendar/calendar-schemas.server"; @@ -23,6 +20,7 @@ import { } from "~/utils/dates"; import { badRequestIfFalsy, + errorToast, errorToastIfFalsy, parseFormData, uploadImageIfSubmitted, @@ -30,6 +28,7 @@ import { import { calendarEventPage } from "~/utils/urls"; import { CALENDAR_EVENT } from "../calendar-constants"; import { canEditCalendarEvent, regClosesAtDate } from "../calendar-utils"; +import { findValidOrganizations } from "../loaders/calendar.new.server"; export const action: ActionFunction = async ({ request }) => { const user = await requireUser(request); @@ -44,11 +43,21 @@ export const action: ActionFunction = async ({ request }) => { parseAsync: true, }); - requireRoleIfNeeded({ - isAddingTournament: data.toToolsEnabled, - isEditing: Boolean(data.eventToEditId), - user, - }); + const isEditing = Boolean(data.eventToEditId); + const isAddingTournament = data.toToolsEnabled; + + if (data.organizationId) { + await validateOrganization({ + userId: user.id, + organizationId: data.organizationId, + isTournamentAdder: user.roles.includes("TOURNAMENT_ADDER"), + }); + } else if (!isEditing) { + requireRole( + user, + isAddingTournament ? "TOURNAMENT_ADDER" : "CALENDAR_EVENT_ADDER", + ); + } const managedBadges = await BadgeRepository.findManagedByUserId(user.id); @@ -188,19 +197,21 @@ export const action: ActionFunction = async ({ request }) => { throw redirect(calendarEventPage(createdEventId)); }; -function requireRoleIfNeeded({ - isAddingTournament, - isEditing, - user, +/** Checks user has permissions to create a tournament in this organization */ +async function validateOrganization({ + userId, + organizationId, + isTournamentAdder, }: { - isAddingTournament: boolean; - isEditing: boolean; - user: AuthenticatedUser; + userId: number; + organizationId: number; + isTournamentAdder: boolean; }) { - if (isEditing) return; + const orgs = await findValidOrganizations(userId, isTournamentAdder); - requireRole( - user, - isAddingTournament ? "TOURNAMENT_ADDER" : "CALENDAR_EVENT_ADDER", + const isValid = orgs.some( + (org) => typeof org !== "string" && org.id === organizationId, ); + + if (!isValid) errorToast("Not authorized to add event for this organization"); } diff --git a/app/features/calendar/loaders/calendar.new.server.ts b/app/features/calendar/loaders/calendar.new.server.ts index 4cda6663e..38850b67e 100644 --- a/app/features/calendar/loaders/calendar.new.server.ts +++ b/app/features/calendar/loaders/calendar.new.server.ts @@ -1,5 +1,6 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; import { redirect } from "@remix-run/node"; +import * as R from "remeda"; import { requireUser } from "~/features/auth/core/user.server"; import * as BadgeRepository from "~/features/badges/BadgeRepository.server"; import * as CalendarRepository from "~/features/calendar/CalendarRepository.server"; @@ -92,7 +93,10 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { ? await CalendarRepository.findRecentTournamentsByAuthorId(user.id) : undefined, organizations: ( - await TournamentOrganizationRepository.findByOrganizerUserId(user.id) + await findValidOrganizations( + user.id, + user.roles.includes("TOURNAMENT_ADDER"), + ) ).concat( eventToEdit?.tournament?.ctx.organization ? eventToEdit.tournament.ctx.organization @@ -100,3 +104,20 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { ), }; }; + +export async function findValidOrganizations( + userId: number, + isTournamentAdder: boolean, +) { + const orgs = await TournamentOrganizationRepository.findByUserId(userId, { + roles: ["ADMIN"], + }); + + if (isTournamentAdder) { + return ["NO_ORG", ...orgs.map((org) => R.omit(org, ["isEstablished"]))]; + } + + return orgs + .filter((org) => org.isEstablished) + .map((org) => R.omit(org, ["isEstablished"])); +} diff --git a/app/features/calendar/routes/calendar.new.tsx b/app/features/calendar/routes/calendar.new.tsx index e8f9ffabc..378f72a2e 100644 --- a/app/features/calendar/routes/calendar.new.tsx +++ b/app/features/calendar/routes/calendar.new.tsx @@ -1,5 +1,5 @@ import type { MetaFunction } from "@remix-run/node"; -import { Form, useFetcher, useLoaderData } from "@remix-run/react"; +import { Form, Link, useFetcher, useLoaderData } from "@remix-run/react"; import clsx from "clsx"; import Compressor from "compressorjs"; import * as React from "react"; @@ -32,7 +32,7 @@ import { import invariant from "~/utils/invariant"; import type { SendouRouteHandle } from "~/utils/remix.server"; import { pathnameFromPotentialURL } from "~/utils/strings"; -import { CREATING_TOURNAMENT_DOC_LINK } from "~/utils/urls"; +import { CREATING_TOURNAMENT_DOC_LINK, FAQ_PAGE } from "~/utils/urls"; import { userSubmittedImage } from "~/utils/urls-img"; import { CALENDAR_EVENT, @@ -80,7 +80,6 @@ const useBaseEvent = () => { export default function CalendarNewEventPage() { const baseEvent = useBaseEvent(); const isCalendarEventAdder = useHasRole("CALENDAR_EVENT_ADDER"); - const isTournamentAdder = useHasRole("TOURNAMENT_ADDER"); const data = useLoaderData(); if (!data.eventToEdit && !isCalendarEventAdder) { @@ -93,12 +92,17 @@ export default function CalendarNewEventPage() { ); } - if (!data.eventToEdit && data.isAddingTournament && !isTournamentAdder) { + if ( + !data.eventToEdit && + data.isAddingTournament && + data.organizations.length === 0 + ) { return (
No permissions to add tournaments. Tournaments are in beta, accessible - by Patreon supporters and established TO's. + by Patreon supporters and established TO's. See{" "} + FAQ for more info.
); @@ -360,14 +364,20 @@ function OrganizationSelect() { ); diff --git a/app/features/info/routes/faq.tsx b/app/features/info/routes/faq.tsx index f9fdcd21e..9f9ebb8a6 100644 --- a/app/features/info/routes/faq.tsx +++ b/app/features/info/routes/faq.tsx @@ -6,7 +6,7 @@ import type { SendouRouteHandle } from "~/utils/remix.server"; import styles from "./faq.module.css"; -const AMOUNT_OF_QUESTIONS = 9; +const AMOUNT_OF_QUESTIONS = 10; export const meta: MetaFunction = (args) => { return metaTags({ diff --git a/app/features/tournament-organization/TournamentOrganizationRepository.server.test.ts b/app/features/tournament-organization/TournamentOrganizationRepository.server.test.ts new file mode 100644 index 000000000..cf3f4b0cd --- /dev/null +++ b/app/features/tournament-organization/TournamentOrganizationRepository.server.test.ts @@ -0,0 +1,92 @@ +import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { dbInsertUsers, dbReset } from "~/utils/Test"; +import * as TournamentOrganizationRepository from "./TournamentOrganizationRepository.server"; + +const createOrganization = async ({ + ownerId, + name, +}: { + ownerId: number; + name: string; +}) => { + return TournamentOrganizationRepository.create({ + ownerId, + name, + }); +}; + +describe("findByUserId", () => { + beforeEach(async () => { + await dbInsertUsers(3); + }); + + afterEach(() => { + dbReset(); + }); + + test("returns organizations where user is a member", async () => { + const org1 = await createOrganization({ + ownerId: 1, + name: "Test Organization 1", + }); + const org2 = await createOrganization({ + ownerId: 1, + name: "Test Organization 2", + }); + + const result = await TournamentOrganizationRepository.findByUserId(1); + + expect(result).toHaveLength(2); + expect(result.map((org) => org.id).sort()).toEqual( + [org1.id, org2.id].sort(), + ); + }); + + test("filters organizations by role when roles parameter is provided", async () => { + const org1 = await createOrganization({ + ownerId: 1, + name: "Test Organization 1", + }); + const org2 = await createOrganization({ + ownerId: 2, + name: "Test Organization 2", + }); + + const org2Data = await TournamentOrganizationRepository.findBySlug( + org2.slug, + ); + + await TournamentOrganizationRepository.update({ + id: org2.id, + name: org2Data!.name, + description: org2Data!.description, + socials: org2Data!.socials, + members: [ + { userId: 2, role: "ADMIN", roleDisplayName: null }, + { userId: 1, role: "ORGANIZER", roleDisplayName: null }, + ], + series: [], + badges: [], + }); + + const adminOrgs = await TournamentOrganizationRepository.findByUserId(1, { + roles: ["ADMIN"], + }); + const allOrgs = await TournamentOrganizationRepository.findByUserId(1); + + expect(adminOrgs).toHaveLength(1); + expect(adminOrgs[0].id).toBe(org1.id); + expect(allOrgs).toHaveLength(2); + }); + + test("returns empty array when user is not a member of any organization", async () => { + await createOrganization({ + ownerId: 1, + name: "Test Organization", + }); + + const result = await TournamentOrganizationRepository.findByUserId(2); + + expect(result).toHaveLength(0); + }); +}); diff --git a/app/features/tournament-organization/TournamentOrganizationRepository.server.ts b/app/features/tournament-organization/TournamentOrganizationRepository.server.ts index e2b624bc9..b2201e62e 100644 --- a/app/features/tournament-organization/TournamentOrganizationRepository.server.ts +++ b/app/features/tournament-organization/TournamentOrganizationRepository.server.ts @@ -57,6 +57,7 @@ export async function findBySlug(slug: string) { "TournamentOrganization.description", "TournamentOrganization.socials", "TournamentOrganization.slug", + "TournamentOrganization.isEstablished", "UserSubmittedImage.url as avatarUrl", jsonArrayFrom( eb @@ -124,7 +125,15 @@ export async function findBySlug(slug: string) { }; } -export function findByOrganizerUserId(userId: number) { +export function findByUserId( + userId: number, + { + roles = [], + }: { + /** If set, filters organizations by user's org member role */ + roles?: Array; + } = {}, +) { return db .selectFrom("TournamentOrganizationMember") .innerJoin( @@ -132,14 +141,14 @@ export function findByOrganizerUserId(userId: number) { "TournamentOrganization.id", "TournamentOrganizationMember.organizationId", ) - .select(["TournamentOrganization.id", "TournamentOrganization.name"]) + .select([ + "TournamentOrganization.id", + "TournamentOrganization.name", + "TournamentOrganization.isEstablished", + ]) .where("TournamentOrganizationMember.userId", "=", userId) - .where((eb) => - eb("TournamentOrganizationMember.role", "=", "ADMIN").or( - "TournamentOrganizationMember.role", - "=", - "ORGANIZER", - ), + .$if(roles.length > 0, (qb) => + qb.where("TournamentOrganizationMember.role", "in", roles), ) .orderBy("TournamentOrganization.id", "asc") .execute(); @@ -532,3 +541,17 @@ export async function countOrganizationsByUserId(userId: number) { return Number(result.count); } + +/** + * Updates the isEstablished status for a tournament organization. + */ +export function updateIsEstablished( + organizationId: number, + isEstablished: boolean, +) { + return db + .updateTable("TournamentOrganization") + .set({ isEstablished: Number(isEstablished) }) + .where("id", "=", organizationId) + .execute(); +} diff --git a/app/features/tournament-organization/actions/org.$slug.server.ts b/app/features/tournament-organization/actions/org.$slug.server.ts index 8733a10dd..6fec08c2d 100644 --- a/app/features/tournament-organization/actions/org.$slug.server.ts +++ b/app/features/tournament-organization/actions/org.$slug.server.ts @@ -1,7 +1,10 @@ import type { ActionFunctionArgs } from "@remix-run/node"; import { isFuture } from "date-fns"; import { requireUser } from "~/features/auth/core/user.server"; -import { requirePermission } from "~/modules/permissions/guards.server"; +import { + requirePermission, + requireRole, +} from "~/modules/permissions/guards.server"; import { databaseTimestampToDate, dateToDatabaseTimestamp, @@ -23,10 +26,10 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { schema: orgPageActionSchema, }); - requirePermission(organization, "BAN", user); - switch (data._action) { case "BAN_USER": { + requirePermission(organization, "BAN", user); + const allBannedUsers = await TournamentOrganizationRepository.allBannedUsersByOrganizationId( organization.id, @@ -60,6 +63,8 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { break; } case "UNBAN_USER": { + requirePermission(organization, "BAN", user); + await TournamentOrganizationRepository.unbanUser({ organizationId: organization.id, userId: data.userId, @@ -71,6 +76,20 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { break; } + case "UPDATE_IS_ESTABLISHED": { + requireRole(user, "ADMIN"); + + await TournamentOrganizationRepository.updateIsEstablished( + organization.id, + data.isEstablished, + ); + + logger.info( + `Organization isEstablished updated: organization=${organization.name} (${organization.id}), isEstablished=${data.isEstablished}, updated by userId=${user.id}`, + ); + + break; + } default: { assertUnreachable(data); } diff --git a/app/features/tournament-organization/routes/org.$slug.tsx b/app/features/tournament-organization/routes/org.$slug.tsx index 02e64ad59..c71122129 100644 --- a/app/features/tournament-organization/routes/org.$slug.tsx +++ b/app/features/tournament-organization/routes/org.$slug.tsx @@ -1,9 +1,15 @@ import type { MetaFunction, SerializeFrom } from "@remix-run/node"; -import { Link, useLoaderData, useSearchParams } from "@remix-run/react"; +import { + Link, + useFetcher, + useLoaderData, + useSearchParams, +} from "@remix-run/react"; import { useTranslation } from "react-i18next"; import { Avatar } from "~/components/Avatar"; import { Divider } from "~/components/Divider"; import { LinkButton } from "~/components/elements/Button"; +import { SendouSwitch } from "~/components/elements/Switch"; import { SendouTab, SendouTabList, @@ -20,7 +26,7 @@ import { Pagination } from "~/components/Pagination"; import { Placement } from "~/components/Placement"; import { BadgeDisplay } from "~/features/badges/components/BadgeDisplay"; import { BannedUsersList } from "~/features/tournament-organization/components/BannedPlayersList"; -import { useHasPermission } from "~/modules/permissions/hooks"; +import { useHasPermission, useHasRole } from "~/modules/permissions/hooks"; import { databaseTimestampNow, databaseTimestampToDate } from "~/utils/dates"; import { metaTags } from "~/utils/remix"; import type { SendouRouteHandle } from "~/utils/remix.server"; @@ -94,6 +100,7 @@ export default function TournamentOrganizationPage() { return (
+ {data.organization.series.length > 0 ? ( @@ -145,6 +152,37 @@ function LogoHeader() { ); } +function AdminControls() { + const data = useLoaderData(); + const fetcher = useFetcher(); + const isAdmin = useHasRole("ADMIN"); + + if (!isAdmin) return null; + + const onChange = (isSelected: boolean) => { + fetcher.submit( + { _action: "UPDATE_IS_ESTABLISHED", isEstablished: isSelected }, + { method: "post", encType: "application/json" }, + ); + }; + + return ( +
+
Admin Controls
+
+ + Is Established Organization + +
+
+ ); +} + function InfoTabs() { const { t } = useTranslation(["org"]); const data = useLoaderData(); diff --git a/app/features/tournament-organization/tournament-organization-schemas.ts b/app/features/tournament-organization/tournament-organization-schemas.ts index 1d6e9b48c..8d45a9bb9 100644 --- a/app/features/tournament-organization/tournament-organization-schemas.ts +++ b/app/features/tournament-organization/tournament-organization-schemas.ts @@ -118,7 +118,13 @@ export const unbanUserActionSchema = z.object({ userId: id, }); +export const updateIsEstablishedActionSchema = z.object({ + _action: _action("UPDATE_IS_ESTABLISHED"), + isEstablished: z.boolean(), +}); + export const orgPageActionSchema = z.union([ banUserActionSchema, unbanUserActionSchema, + updateIsEstablishedActionSchema, ]); diff --git a/db-test.sqlite3 b/db-test.sqlite3 index dc4dcb82e804423b69297d449f40221a37d82518..1c927f2750cdd5beb02b691b135443c12fa4fba7 100644 GIT binary patch delta 467 zcmZp8;LyI}mdKG3WME z{9INGm<5=$wzF^G`t@Iz)rUcl;Ufdv5~gjuUA%@ovv}CJr*gY-Z0Bg>P-Q>G9?$la z;UmjmmK+8jZpF!h4sz3dHgGQ$Xm)JOVi&ixWNMQxNleN~ogR3XM{Ii0R_^b73QC#9 zuEikH%;JpH>5dYN!qW~j3QWJgnOkUj!#3{J?Zz9pwlPl6*~0w*=zYeB?Ezc4Ll~#O z+{oqF{&XAn_NUu;PaF4Bu3(Of+hs2BOl8>)bcF&- z`^6hPK+L=S;tf9Ia%MrE*y-#6{EAFGv6}@QjCpLW(^(iojSUTqbW8F}i}Dh4Q}ar6 z^NZ4TGmCXo;Z97^%PKZ9Ff!FOG|)9NS1>fTGB&a@GT1&VhCl7{BnK8TPWDCy{*`== dd~DqNxkI>eImyHUk4AGXoqjmQU0%=9$f)*QUt|6y)RIG%;I_bqD_@rr8_&bBPi-0nBoA`m`xyE`Rxu-E7NUmv&Z9c`{eu|$Fh?#(x8Hibcm=%cGfS4VKIe?gR z`zd}ds|CzL4945pH*o#>&kocr#Nfj)oo55L1Rtvp%U_lprfenw1|M$4$$}1Y(|tB@ zFP$E+nVV&M)K#8x=IJ?GxF4`Ii#BF$57^2b!Z>H2EStkaio;^uFE zx{Z7L(``KRk`e+8K@9xMdGGNG^R#h4 { await expect(page.getByText("Teams (1)")).toBeVisible(); }); + + test("allows member of established org to create tournament", async ({ + page, + }) => { + const ORG_ADMIN_ID = 3; // 3 = org admin, but not site admin + + await seed(page); + + await impersonate(page, ORG_ADMIN_ID); + await navigate({ + page, + url: TOURNAMENT_NEW_PAGE, + }); + await expect( + page.getByText("No permissions to add tournaments"), + ).toBeVisible(); + await expect(page.getByText("New tournament")).not.toBeVisible(); + + await impersonate(page, ADMIN_ID); + await navigate({ + page, + url: "/org/sendouink", + }); + + await page.getByTestId("is-established-switch").click(); + + await impersonate(page, ORG_ADMIN_ID); + await navigate({ + page, + url: TOURNAMENT_NEW_PAGE, + }); + + await expect( + page.getByText("No permissions to add tournaments"), + ).not.toBeVisible(); + await expect(page.getByText("New tournament")).toBeVisible(); + }); }); diff --git a/locales/da/faq.json b/locales/da/faq.json index 1a95557e0..9e2e66d4a 100644 --- a/locales/da/faq.json +++ b/locales/da/faq.json @@ -16,5 +16,7 @@ "q8": "Hvordan kan jeg finde mine X-kampresultater på min profil?", "a8": "", "q9": "Hvordan kan jeg uploade kunst, som jeg har bestilt", - "a9": "Du kan kun uploade kunst, som du selv har lavet. Det skal også være Splatoon-relateret. For bestilte kunstværker, så henvend dig til kunstneren, så De kan uploade det til hjemmesiden. De kan også knytte kunstværket til din profil, så det bliver vist på din profil" + "a9": "Du kan kun uploade kunst, som du selv har lavet. Det skal også være Splatoon-relateret. For bestilte kunstværker, så henvend dig til kunstneren, så De kan uploade det til hjemmesiden. De kan også knytte kunstværket til din profil, så det bliver vist på din profil", + "q10:": "", + "a10": "" } diff --git a/locales/de/faq.json b/locales/de/faq.json index 7c8f68673..733d07c5f 100644 --- a/locales/de/faq.json +++ b/locales/de/faq.json @@ -16,5 +16,7 @@ "q8": "", "a8": "", "q9": "", - "a9": "" + "a9": "", + "q10:": "", + "a10": "" } diff --git a/locales/en/faq.json b/locales/en/faq.json index 8393dabee..5b6a29c87 100644 --- a/locales/en/faq.json +++ b/locales/en/faq.json @@ -16,5 +16,7 @@ "q8": "How can I show X Battle results on my profile?", "a8": "Find your player page from the Top Search page. Post the link to the helpdesk on our Discord saying you want it linked. Note that the linking is not possible if you have not finished a season in the Top 500.", "q9": "How can I upload art that I commissioned?", - "a9": "You can only upload art that you made yourself. It also has to be Splatoon related. For commissioned work please ask the artist to upload it themselves to the site. They are able to link your user profile and that will make it show on your profile." + "a9": "You can only upload art that you made yourself. It also has to be Splatoon related. For commissioned work please ask the artist to upload it themselves to the site. They are able to link your user profile and that will make it show on your profile.", + "q10": "What are established tournament organizations?", + "a10": "These are organizations that have permissions to host tournaments on sendou.ink. To become an established organization you need 150 monthly active users in your tournaments over 6 month period (checked in June and December). This can be either on sendou.ink or another platform. In addition patrons (of Supporter tier or above) have the tournament permissions." } diff --git a/locales/es-ES/faq.json b/locales/es-ES/faq.json index 43b633c21..9b481eac8 100644 --- a/locales/es-ES/faq.json +++ b/locales/es-ES/faq.json @@ -16,5 +16,7 @@ "q8": "¿Cómo puedo mostrar mis resultados de Combate X en mi perfil?", "a8": "", "q9": "¿Cómo puedo subir arte que he comisionado?", - "a9": "Solo puedes agregar arte que tú has creado. También tiene que tener relación con Splatoon. Para piezas comisionadas, por favor pide al artista que suba el arte. El artista puede enlazar tu perfil y así se podrá mostrar en tu perfil." + "a9": "Solo puedes agregar arte que tú has creado. También tiene que tener relación con Splatoon. Para piezas comisionadas, por favor pide al artista que suba el arte. El artista puede enlazar tu perfil y así se podrá mostrar en tu perfil.", + "q10:": "", + "a10": "" } diff --git a/locales/es-US/faq.json b/locales/es-US/faq.json index 43b633c21..9b481eac8 100644 --- a/locales/es-US/faq.json +++ b/locales/es-US/faq.json @@ -16,5 +16,7 @@ "q8": "¿Cómo puedo mostrar mis resultados de Combate X en mi perfil?", "a8": "", "q9": "¿Cómo puedo subir arte que he comisionado?", - "a9": "Solo puedes agregar arte que tú has creado. También tiene que tener relación con Splatoon. Para piezas comisionadas, por favor pide al artista que suba el arte. El artista puede enlazar tu perfil y así se podrá mostrar en tu perfil." + "a9": "Solo puedes agregar arte que tú has creado. También tiene que tener relación con Splatoon. Para piezas comisionadas, por favor pide al artista que suba el arte. El artista puede enlazar tu perfil y así se podrá mostrar en tu perfil.", + "q10:": "", + "a10": "" } diff --git a/locales/fr-CA/faq.json b/locales/fr-CA/faq.json index f35cdbc47..5146c448f 100644 --- a/locales/fr-CA/faq.json +++ b/locales/fr-CA/faq.json @@ -16,5 +16,7 @@ "q8": "Comment puis-je montrer mes résultats de Match X sur mon profil ?", "a8": "", "q9": "", - "a9": "" + "a9": "", + "q10:": "", + "a10": "" } diff --git a/locales/fr-EU/faq.json b/locales/fr-EU/faq.json index eea0c7368..8788a0a49 100644 --- a/locales/fr-EU/faq.json +++ b/locales/fr-EU/faq.json @@ -16,5 +16,7 @@ "q8": "Comment puis-je montrer mes résultats de Match X sur mon profil ?", "a8": "Trouvez votre page de joueur depuis la page Top 500. Postez le lien vers le channel helpdesk sur notre Discord en indiquant que vous souhaitez qu'il soit lié. Note: Le lien n'est pas possible si vous n'avez pas terminé une saison dans le Top 500.", "q9": "Comment puis-je publier une œuvre que j'ai commandé ?", - "a9": "Vous pouvez seulement publier une œuvre que vous avez réalisé. Il doit également y avoir un rapport avec Splatoon. Veuillez demander à l’artiste de la télécharger lui-même sur le site pour une œuvre commandée. Ils sont capables de lier votre profil d'utilisateur à leur œuvre et cela la fera apparaître sur votre profil." + "a9": "Vous pouvez seulement publier une œuvre que vous avez réalisé. Il doit également y avoir un rapport avec Splatoon. Veuillez demander à l’artiste de la télécharger lui-même sur le site pour une œuvre commandée. Ils sont capables de lier votre profil d'utilisateur à leur œuvre et cela la fera apparaître sur votre profil.", + "q10:": "", + "a10": "" } diff --git a/locales/he/faq.json b/locales/he/faq.json index fe7077223..0d6b5ea3f 100644 --- a/locales/he/faq.json +++ b/locales/he/faq.json @@ -16,5 +16,7 @@ "q8": "כיצד אוכל להציג תוצאות X Battle בפרופיל שלי?", "a8": "", "q9": "איך אני יכול להעלות ציור שאני ביקשתי?", - "a9": "ניתן להעלות ציור שהכנת בעצמך. הוא צריך להיות קשור ל-Splatoon. בשביל עבודה שביקשת נא לבקש מהאומן להעלות את הציור בכוחות עצמם לאתר. הם יכולים לקשר את הפרופיל שלך מה שיגרום לו להופיע שם." + "a9": "ניתן להעלות ציור שהכנת בעצמך. הוא צריך להיות קשור ל-Splatoon. בשביל עבודה שביקשת נא לבקש מהאומן להעלות את הציור בכוחות עצמם לאתר. הם יכולים לקשר את הפרופיל שלך מה שיגרום לו להופיע שם.", + "q10:": "", + "a10": "" } diff --git a/locales/it/faq.json b/locales/it/faq.json index bc105fd6d..3b985186d 100644 --- a/locales/it/faq.json +++ b/locales/it/faq.json @@ -16,5 +16,7 @@ "q8": "Come faccio a mostrare i risultati delle Partite X sul mio profilo?", "a8": "Cerca la tua pagina profilo sulla pagina Ricerca Top. Posta il link sull'helpdesk nel nostro Discord dicendo di volerla associata. L'associazione non è possibile se non hai concluso una stagione in Top 500.", "q9": "Come faccio a caricare un'opera che ho commissionato?", - "a9": "Puoi caricare solo art che hai creato tu stesso/a. Deve anche essere relativa a Splatoon. Per opere commissionate, si prega di chiedere all'artista originale di caricarle. Possono collegare il tuo profilo e fare in modo che esse vengano mostrate lì." + "a9": "Puoi caricare solo art che hai creato tu stesso/a. Deve anche essere relativa a Splatoon. Per opere commissionate, si prega di chiedere all'artista originale di caricarle. Possono collegare il tuo profilo e fare in modo che esse vengano mostrate lì.", + "q10:": "", + "a10": "" } diff --git a/locales/ja/faq.json b/locales/ja/faq.json index 2f6721f10..78f4d191a 100644 --- a/locales/ja/faq.json +++ b/locales/ja/faq.json @@ -16,5 +16,7 @@ "q8": "X ランクのバトル結果を自分のプロファイルに表示するにはどうしたらよいですか?", "a8": "", "q9": "依頼した作品はどうやってアップロードできますか?", - "a9": "自分で作った作品のみアップロードできます(もちろんスプラトゥーン関連でないといけません)。依頼した作品の場合はその作品の作者からアップロードするように伝えてください。そうすることによって自分のプロファイルに作品が表示されます。" + "a9": "自分で作った作品のみアップロードできます(もちろんスプラトゥーン関連でないといけません)。依頼した作品の場合はその作品の作者からアップロードするように伝えてください。そうすることによって自分のプロファイルに作品が表示されます。", + "q10:": "", + "a10": "" } diff --git a/locales/ko/faq.json b/locales/ko/faq.json index bcf7ef074..7d5876821 100644 --- a/locales/ko/faq.json +++ b/locales/ko/faq.json @@ -16,5 +16,7 @@ "q8": "", "a8": "", "q9": "", - "a9": "" + "a9": "", + "q10:": "", + "a10": "" } diff --git a/locales/nl/faq.json b/locales/nl/faq.json index 7d79d81b9..d5bf38ba0 100644 --- a/locales/nl/faq.json +++ b/locales/nl/faq.json @@ -16,5 +16,7 @@ "q8": "", "a8": "", "q9": "", - "a9": "" + "a9": "", + "q10:": "", + "a10": "" } diff --git a/locales/pl/faq.json b/locales/pl/faq.json index 3c16e99bf..680d36cb3 100644 --- a/locales/pl/faq.json +++ b/locales/pl/faq.json @@ -16,5 +16,7 @@ "q8": "", "a8": "", "q9": "", - "a9": "" + "a9": "", + "q10:": "", + "a10": "" } diff --git a/locales/pt-BR/faq.json b/locales/pt-BR/faq.json index f62598b51..6efeb904b 100644 --- a/locales/pt-BR/faq.json +++ b/locales/pt-BR/faq.json @@ -16,5 +16,7 @@ "q8": "Como posso mostrar meus resultados do X Battle no meu perfil?", "a8": "", "q9": "Como posso fazer o upload de uma arte da qual fiz a comissão?", - "a9": "Você só pode fazer upload de arte que você mesmo(a) fez. Ela também tem que ser relacionada ao Splatoon. Para trabalhos comissionados, por favor peça o artista para fazer o upload no site. Eles são capazes de conectar a arte ao seu perfil de usuário e isso fará que ela apareça no seu perfil." + "a9": "Você só pode fazer upload de arte que você mesmo(a) fez. Ela também tem que ser relacionada ao Splatoon. Para trabalhos comissionados, por favor peça o artista para fazer o upload no site. Eles são capazes de conectar a arte ao seu perfil de usuário e isso fará que ela apareça no seu perfil.", + "q10:": "", + "a10": "" } diff --git a/locales/ru/faq.json b/locales/ru/faq.json index 41bb81d35..68574bc5c 100644 --- a/locales/ru/faq.json +++ b/locales/ru/faq.json @@ -16,5 +16,7 @@ "q8": "Как разместить результаты Боёв X на моём профиле?", "a8": "Найдите вашу пользовательскую страницу в секции Топ по режиму. Отправьте сообщение в helpdesk канал на Discord сервере sendou.ink с просьбой привязать эту ссылку к вашему профилю. Обратите внимание, что это можно сделать только если вы закончили сезон в Топ 500.", "q9": "Как разместить арт, который я заказал?", - "a9": "Вы можете разместить только арт, созданный непосредственно вами. Он также должен быть по тематике Splatoon. Для артов по заказу попросите художника-автора загрузить их на свою страницу. После того, как художник загрузит работу и отметит вас в ней, арт появится на вашем профиле." + "a9": "Вы можете разместить только арт, созданный непосредственно вами. Он также должен быть по тематике Splatoon. Для артов по заказу попросите художника-автора загрузить их на свою страницу. После того, как художник загрузит работу и отметит вас в ней, арт появится на вашем профиле.", + "q10:": "", + "a10": "" } diff --git a/locales/zh/faq.json b/locales/zh/faq.json index f07773dc7..ef8015567 100644 --- a/locales/zh/faq.json +++ b/locales/zh/faq.json @@ -16,5 +16,7 @@ "q8": "我应该如何在我的资料中显示X比赛结果?", "a8": "", "q9": "我应该如何上传委托他人创作的艺术作品?", - "a9": "您只能上传您自己创作的艺术作品。并且需要与斯普拉遁相关。对于委托他人创作的作品,请联系作者亲自上传。他们可以将作品链接到您的个人信息,并展示在您的个人信息页。" + "a9": "您只能上传您自己创作的艺术作品。并且需要与斯普拉遁相关。对于委托他人创作的作品,请联系作者亲自上传。他们可以将作品链接到您的个人信息,并展示在您的个人信息页。", + "q10:": "", + "a10": "" } diff --git a/migrations/102-tournament-org-is-established.js b/migrations/102-tournament-org-is-established.js new file mode 100644 index 000000000..8895d0dca --- /dev/null +++ b/migrations/102-tournament-org-is-established.js @@ -0,0 +1,8 @@ +export function up(db) { + db.transaction(() => { + db.prepare( + /* sql */ `alter table "TournamentOrganization" add "isEstablished" integer not null default 0`, + ).run(); + db.prepare(/* sql */ `update "User" set "isTournamentOrganizer" = 0`).run(); + })(); +}