mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-20 10:04:57 -05:00
Migrate edit org page to the new image SendouForm field component
This commit is contained in:
@@ -3,13 +3,9 @@ import { databaseTimestampNow } from "~/utils/dates";
|
||||
import { dbInsertUsers, dbReset } from "~/utils/Test";
|
||||
import * as ArtRepository from "../art/ArtRepository.server";
|
||||
import * as CalendarRepository from "../calendar/CalendarRepository.server";
|
||||
import * as TeamRepository from "../team/TeamRepository.server";
|
||||
import * as TournamentOrganizationRepository from "../tournament-organization/TournamentOrganizationRepository.server";
|
||||
import * as ImageRepository from "./ImageRepository.server";
|
||||
|
||||
let imageCounter = 0;
|
||||
let teamCounter = 0;
|
||||
let orgCounter = 0;
|
||||
|
||||
const createImage = async ({
|
||||
submitterUserId,
|
||||
@@ -21,31 +17,24 @@ const createImage = async ({
|
||||
imageCounter++;
|
||||
const url = `image-${submitterUserId}-${imageCounter}.png`;
|
||||
|
||||
return ImageRepository.addNewImage({
|
||||
submitterUserId,
|
||||
url,
|
||||
return ImageRepository.insert({ submitterUserId, url, validatedAt });
|
||||
};
|
||||
|
||||
const createArtImage = async ({
|
||||
authorId,
|
||||
validatedAt = null,
|
||||
}: {
|
||||
authorId: number;
|
||||
validatedAt?: number | null;
|
||||
}) => {
|
||||
imageCounter++;
|
||||
return ArtRepository.insert({
|
||||
authorId,
|
||||
url: `art-${imageCounter}.png`,
|
||||
validatedAt,
|
||||
type: "team-pfp",
|
||||
});
|
||||
};
|
||||
|
||||
const createTeam = async (ownerUserId: number) => {
|
||||
teamCounter++;
|
||||
const createdTeam = await TeamRepository.create({
|
||||
name: `Team ${teamCounter}`,
|
||||
ownerUserId,
|
||||
isMainTeam: true,
|
||||
});
|
||||
const team = await TeamRepository.findByCustomUrl(createdTeam.customUrl);
|
||||
if (!team) throw new Error("Team not found after creation");
|
||||
return team;
|
||||
};
|
||||
|
||||
const createOrganization = async (ownerId: number) => {
|
||||
orgCounter++;
|
||||
return TournamentOrganizationRepository.create({
|
||||
name: `Org ${orgCounter}`,
|
||||
ownerId,
|
||||
description: null,
|
||||
linkedUsers: [],
|
||||
tags: [],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -124,25 +113,15 @@ describe("deleteImageById", () => {
|
||||
});
|
||||
|
||||
test("deletes associated art when deleting image", async () => {
|
||||
imageCounter++;
|
||||
const art = await ArtRepository.insert({
|
||||
authorId: 1,
|
||||
url: `art-${imageCounter}.png`,
|
||||
validatedAt: Date.now(),
|
||||
description: null,
|
||||
linkedUsers: [],
|
||||
tags: [],
|
||||
});
|
||||
const art = await createArtImage({ authorId: 1, validatedAt: Date.now() });
|
||||
|
||||
const artsBefore = await ArtRepository.findArtsByUserId(1);
|
||||
expect(artsBefore).toHaveLength(1);
|
||||
expect(artsBefore[0].id).toBe(art.id);
|
||||
|
||||
const imgId = art.imgId;
|
||||
await ImageRepository.deleteImageById(art.imgId);
|
||||
|
||||
await ImageRepository.deleteImageById(imgId);
|
||||
|
||||
const result = await ImageRepository.findById(imgId);
|
||||
const result = await ImageRepository.findById(art.imgId);
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
const artsAfter = await ArtRepository.findArtsByUserId(1);
|
||||
@@ -161,25 +140,8 @@ describe("countUnvalidatedArt", () => {
|
||||
});
|
||||
|
||||
test("counts unvalidated art by author", async () => {
|
||||
imageCounter++;
|
||||
await ArtRepository.insert({
|
||||
authorId: 1,
|
||||
url: `art-${imageCounter}.png`,
|
||||
validatedAt: null,
|
||||
description: null,
|
||||
linkedUsers: [],
|
||||
tags: [],
|
||||
});
|
||||
|
||||
imageCounter++;
|
||||
await ArtRepository.insert({
|
||||
authorId: 1,
|
||||
url: `art-${imageCounter}.png`,
|
||||
validatedAt: null,
|
||||
description: null,
|
||||
linkedUsers: [],
|
||||
tags: [],
|
||||
});
|
||||
await createArtImage({ authorId: 1 });
|
||||
await createArtImage({ authorId: 1 });
|
||||
|
||||
const count = await ImageRepository.countUnvalidatedArt(1);
|
||||
|
||||
@@ -187,25 +149,8 @@ describe("countUnvalidatedArt", () => {
|
||||
});
|
||||
|
||||
test("does not count validated art", async () => {
|
||||
imageCounter++;
|
||||
await ArtRepository.insert({
|
||||
authorId: 1,
|
||||
url: `art-${imageCounter}.png`,
|
||||
validatedAt: null,
|
||||
description: null,
|
||||
linkedUsers: [],
|
||||
tags: [],
|
||||
});
|
||||
|
||||
imageCounter++;
|
||||
await ArtRepository.insert({
|
||||
authorId: 1,
|
||||
url: `art-${imageCounter}.png`,
|
||||
validatedAt: Date.now(),
|
||||
description: null,
|
||||
linkedUsers: [],
|
||||
tags: [],
|
||||
});
|
||||
await createArtImage({ authorId: 1 });
|
||||
await createArtImage({ authorId: 1, validatedAt: Date.now() });
|
||||
|
||||
const count = await ImageRepository.countUnvalidatedArt(1);
|
||||
|
||||
@@ -222,7 +167,6 @@ describe("countUnvalidatedArt", () => {
|
||||
describe("countAllUnvalidated", () => {
|
||||
beforeEach(async () => {
|
||||
imageCounter = 0;
|
||||
teamCounter = 0;
|
||||
await dbInsertUsers(3);
|
||||
});
|
||||
|
||||
@@ -230,32 +174,8 @@ describe("countAllUnvalidated", () => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("counts unvalidated images used in teams", async () => {
|
||||
const team = await createTeam(1);
|
||||
imageCounter++;
|
||||
await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
url: `team-avatar-${imageCounter}.png`,
|
||||
validatedAt: null,
|
||||
teamId: team.id,
|
||||
type: "team-pfp",
|
||||
});
|
||||
|
||||
const count = await ImageRepository.countAllUnvalidated();
|
||||
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
test("counts unvalidated images used in art", async () => {
|
||||
imageCounter++;
|
||||
await ArtRepository.insert({
|
||||
authorId: 1,
|
||||
url: `art-${imageCounter}.png`,
|
||||
validatedAt: null,
|
||||
description: null,
|
||||
linkedUsers: [],
|
||||
tags: [],
|
||||
});
|
||||
await createArtImage({ authorId: 1 });
|
||||
|
||||
const count = await ImageRepository.countAllUnvalidated();
|
||||
|
||||
@@ -272,15 +192,7 @@ describe("countAllUnvalidated", () => {
|
||||
});
|
||||
|
||||
test("does not count validated images", async () => {
|
||||
const team = await createTeam(1);
|
||||
imageCounter++;
|
||||
await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
url: `team-avatar-${imageCounter}.png`,
|
||||
validatedAt: Date.now(),
|
||||
teamId: team.id,
|
||||
type: "team-pfp",
|
||||
});
|
||||
await createArtImage({ authorId: 1, validatedAt: Date.now() });
|
||||
|
||||
const count = await ImageRepository.countAllUnvalidated();
|
||||
|
||||
@@ -288,25 +200,10 @@ describe("countAllUnvalidated", () => {
|
||||
});
|
||||
|
||||
test("counts multiple unvalidated images across different types", async () => {
|
||||
const team = await createTeam(1);
|
||||
imageCounter++;
|
||||
await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
url: `team-avatar-${imageCounter}.png`,
|
||||
validatedAt: null,
|
||||
teamId: team.id,
|
||||
type: "team-pfp",
|
||||
});
|
||||
await createArtImage({ authorId: 1 });
|
||||
|
||||
imageCounter++;
|
||||
await ArtRepository.insert({
|
||||
authorId: 1,
|
||||
url: `art-${imageCounter}.png`,
|
||||
validatedAt: null,
|
||||
description: null,
|
||||
linkedUsers: [],
|
||||
tags: [],
|
||||
});
|
||||
const img = await createImage({ submitterUserId: 1 });
|
||||
await createCalendarEvent(1, img.id);
|
||||
|
||||
const count = await ImageRepository.countAllUnvalidated();
|
||||
|
||||
@@ -323,7 +220,6 @@ describe("countAllUnvalidated", () => {
|
||||
describe("countUnvalidatedBySubmitterUserId", () => {
|
||||
beforeEach(async () => {
|
||||
imageCounter = 0;
|
||||
teamCounter = 0;
|
||||
await dbInsertUsers(3);
|
||||
});
|
||||
|
||||
@@ -331,87 +227,11 @@ describe("countUnvalidatedBySubmitterUserId", () => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("counts unvalidated team images by submitter", async () => {
|
||||
const team = await createTeam(1);
|
||||
imageCounter++;
|
||||
await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
url: `team-avatar-${imageCounter}.png`,
|
||||
validatedAt: null,
|
||||
teamId: team.id,
|
||||
type: "team-pfp",
|
||||
});
|
||||
|
||||
const count = await ImageRepository.countUnvalidatedBySubmitterUserId(1);
|
||||
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
test("does not count validated images", async () => {
|
||||
const team = await createTeam(1);
|
||||
imageCounter++;
|
||||
await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
url: `team-avatar-${imageCounter}.png`,
|
||||
validatedAt: Date.now(),
|
||||
teamId: team.id,
|
||||
type: "team-pfp",
|
||||
});
|
||||
|
||||
const count = await ImageRepository.countUnvalidatedBySubmitterUserId(1);
|
||||
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
test("does not count images from other submitters", async () => {
|
||||
const team1 = await createTeam(1);
|
||||
const team2 = await createTeam(2);
|
||||
|
||||
imageCounter++;
|
||||
await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
url: `team1-avatar-${imageCounter}.png`,
|
||||
validatedAt: null,
|
||||
teamId: team1.id,
|
||||
type: "team-pfp",
|
||||
});
|
||||
|
||||
imageCounter++;
|
||||
await ImageRepository.addNewImage({
|
||||
submitterUserId: 2,
|
||||
url: `team2-avatar-${imageCounter}.png`,
|
||||
validatedAt: null,
|
||||
teamId: team2.id,
|
||||
type: "team-pfp",
|
||||
});
|
||||
|
||||
const count = await ImageRepository.countUnvalidatedBySubmitterUserId(1);
|
||||
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
test("returns 0 when user has no unvalidated team images", async () => {
|
||||
const count = await ImageRepository.countUnvalidatedBySubmitterUserId(1);
|
||||
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("countAllUnvalidatedBySubmitterUserId", () => {
|
||||
beforeEach(async () => {
|
||||
imageCounter = 0;
|
||||
await dbInsertUsers(3);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("counts unvalidated images not associated with any entity", async () => {
|
||||
test("counts unvalidated images by submitter", async () => {
|
||||
await createImage({ submitterUserId: 1 });
|
||||
await createImage({ submitterUserId: 1 });
|
||||
|
||||
const count = await ImageRepository.countAllUnvalidatedBySubmitterUserId(1);
|
||||
const count = await ImageRepository.countUnvalidatedBySubmitterUserId(1);
|
||||
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
@@ -419,7 +239,7 @@ describe("countAllUnvalidatedBySubmitterUserId", () => {
|
||||
test("does not count validated images", async () => {
|
||||
await createImage({ submitterUserId: 1, validatedAt: Date.now() });
|
||||
|
||||
const count = await ImageRepository.countAllUnvalidatedBySubmitterUserId(1);
|
||||
const count = await ImageRepository.countUnvalidatedBySubmitterUserId(1);
|
||||
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
@@ -428,13 +248,13 @@ describe("countAllUnvalidatedBySubmitterUserId", () => {
|
||||
await createImage({ submitterUserId: 1 });
|
||||
await createImage({ submitterUserId: 2 });
|
||||
|
||||
const count = await ImageRepository.countAllUnvalidatedBySubmitterUserId(1);
|
||||
const count = await ImageRepository.countUnvalidatedBySubmitterUserId(1);
|
||||
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
test("returns 0 when user has no unvalidated images", async () => {
|
||||
const count = await ImageRepository.countAllUnvalidatedBySubmitterUserId(1);
|
||||
const count = await ImageRepository.countUnvalidatedBySubmitterUserId(1);
|
||||
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
@@ -460,20 +280,12 @@ describe("validateImage", () => {
|
||||
});
|
||||
|
||||
test("validated image is not included in unvalidated count", async () => {
|
||||
const team = await createTeam(1);
|
||||
imageCounter++;
|
||||
const img = await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
url: `team-avatar-${imageCounter}.png`,
|
||||
validatedAt: null,
|
||||
teamId: team.id,
|
||||
type: "team-pfp",
|
||||
});
|
||||
const art = await createArtImage({ authorId: 1 });
|
||||
|
||||
const countBefore = await ImageRepository.countAllUnvalidated();
|
||||
expect(countBefore).toBe(1);
|
||||
|
||||
await ImageRepository.validateImage(img.id);
|
||||
await ImageRepository.validateImage(art.imgId);
|
||||
|
||||
const countAfter = await ImageRepository.countAllUnvalidated();
|
||||
expect(countAfter).toBe(0);
|
||||
@@ -483,8 +295,7 @@ describe("validateImage", () => {
|
||||
describe("unvalidatedImages", () => {
|
||||
beforeEach(async () => {
|
||||
imageCounter = 0;
|
||||
teamCounter = 0;
|
||||
await dbInsertUsers(10);
|
||||
await dbInsertUsers(3);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -492,15 +303,15 @@ describe("unvalidatedImages", () => {
|
||||
});
|
||||
|
||||
test("fetches unvalidated images with submitter info", async () => {
|
||||
const team = await createTeam(1);
|
||||
imageCounter++;
|
||||
const filename = `team-avatar-${imageCounter}.png`;
|
||||
await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
const filename = `art-${imageCounter}.png`;
|
||||
await ArtRepository.insert({
|
||||
authorId: 1,
|
||||
url: filename,
|
||||
validatedAt: null,
|
||||
teamId: team.id,
|
||||
type: "team-pfp",
|
||||
description: null,
|
||||
linkedUsers: [],
|
||||
tags: [],
|
||||
});
|
||||
|
||||
const result = await ImageRepository.unvalidatedImages();
|
||||
@@ -512,44 +323,19 @@ describe("unvalidatedImages", () => {
|
||||
});
|
||||
|
||||
test("does not fetch validated images", async () => {
|
||||
const team = await createTeam(1);
|
||||
imageCounter++;
|
||||
await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
url: `team-avatar-${imageCounter}.png`,
|
||||
validatedAt: Date.now(),
|
||||
teamId: team.id,
|
||||
type: "team-pfp",
|
||||
});
|
||||
await createArtImage({ authorId: 1, validatedAt: Date.now() });
|
||||
|
||||
const result = await ImageRepository.unvalidatedImages();
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("fetches images from teams, art, and calendar events", async () => {
|
||||
const team = await createTeam(1);
|
||||
imageCounter++;
|
||||
await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
url: `team-avatar-${imageCounter}.png`,
|
||||
validatedAt: null,
|
||||
teamId: team.id,
|
||||
type: "team-pfp",
|
||||
});
|
||||
test("fetches images from art and calendar events", async () => {
|
||||
await createArtImage({ authorId: 1 });
|
||||
await createArtImage({ authorId: 2 });
|
||||
|
||||
imageCounter++;
|
||||
await ArtRepository.insert({
|
||||
authorId: 2,
|
||||
url: `art-${imageCounter}.png`,
|
||||
validatedAt: null,
|
||||
description: null,
|
||||
linkedUsers: [],
|
||||
tags: [],
|
||||
});
|
||||
|
||||
const img2 = await createImage({ submitterUserId: 3 });
|
||||
await createCalendarEvent(3, img2.id);
|
||||
const img = await createImage({ submitterUserId: 3 });
|
||||
await createCalendarEvent(3, img.id);
|
||||
|
||||
const result = await ImageRepository.unvalidatedImages();
|
||||
|
||||
@@ -558,16 +344,7 @@ describe("unvalidatedImages", () => {
|
||||
|
||||
test("respects the max unvalidated images to show at once for approval limit constant", async () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const teamOwnerId = i + 1;
|
||||
const team = await createTeam(teamOwnerId);
|
||||
|
||||
await ImageRepository.addNewImage({
|
||||
submitterUserId: teamOwnerId,
|
||||
url: `team-avatar-${i}.png`,
|
||||
validatedAt: null,
|
||||
teamId: team.id,
|
||||
type: "team-pfp",
|
||||
});
|
||||
await createArtImage({ authorId: 1 });
|
||||
}
|
||||
|
||||
const result = await ImageRepository.unvalidatedImages();
|
||||
@@ -581,114 +358,3 @@ describe("unvalidatedImages", () => {
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addNewImage", () => {
|
||||
beforeEach(async () => {
|
||||
imageCounter = 0;
|
||||
teamCounter = 0;
|
||||
orgCounter = 0;
|
||||
await dbInsertUsers(3);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("creates image for team avatar", async () => {
|
||||
const team = await createTeam(1);
|
||||
imageCounter++;
|
||||
const filename = `team-avatar-${imageCounter}.png`;
|
||||
|
||||
const img = await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
url: filename,
|
||||
validatedAt: null,
|
||||
teamId: team.id,
|
||||
type: "team-pfp",
|
||||
});
|
||||
|
||||
expect(img.url).toBe(filename);
|
||||
expect(img.submitterUserId).toBe(1);
|
||||
expect(img.validatedAt).toBeNull();
|
||||
|
||||
const result = await ImageRepository.findById(img.id);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
test("creates image for team banner", async () => {
|
||||
const team = await createTeam(1);
|
||||
imageCounter++;
|
||||
const filename = `team-banner-${imageCounter}.png`;
|
||||
|
||||
const img = await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
url: filename,
|
||||
validatedAt: null,
|
||||
teamId: team.id,
|
||||
type: "team-banner",
|
||||
});
|
||||
|
||||
expect(img.url).toBe(filename);
|
||||
expect(img.submitterUserId).toBe(1);
|
||||
expect(img.validatedAt).toBeNull();
|
||||
|
||||
const result = await ImageRepository.findById(img.id);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
test("creates image for organization avatar", async () => {
|
||||
const org = await createOrganization(1);
|
||||
imageCounter++;
|
||||
const filename = `org-avatar-${imageCounter}.png`;
|
||||
|
||||
const img = await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
url: filename,
|
||||
validatedAt: null,
|
||||
organizationId: org.id,
|
||||
type: "org-pfp",
|
||||
});
|
||||
|
||||
expect(img.url).toBe(filename);
|
||||
expect(img.submitterUserId).toBe(1);
|
||||
expect(img.validatedAt).toBeNull();
|
||||
|
||||
const result = await ImageRepository.findById(img.id);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
test("creates validated image when validatedAt is provided", async () => {
|
||||
const team = await createTeam(1);
|
||||
const validatedAt = Date.now();
|
||||
imageCounter++;
|
||||
|
||||
const img = await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
url: `team-avatar-${imageCounter}.png`,
|
||||
validatedAt,
|
||||
teamId: team.id,
|
||||
type: "team-pfp",
|
||||
});
|
||||
|
||||
expect(img.validatedAt).toBe(validatedAt);
|
||||
|
||||
const count = await ImageRepository.countAllUnvalidated();
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
test("creates unvalidated image when validatedAt is null", async () => {
|
||||
const team = await createTeam(1);
|
||||
imageCounter++;
|
||||
|
||||
await ImageRepository.addNewImage({
|
||||
submitterUserId: 1,
|
||||
url: `team-avatar-${imageCounter}.png`,
|
||||
validatedAt: null,
|
||||
teamId: team.id,
|
||||
type: "team-pfp",
|
||||
});
|
||||
|
||||
const count = await ImageRepository.countAllUnvalidated();
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { DB, TablesInsertable } from "~/db/tables";
|
||||
import { databaseTimestampNow } from "~/utils/dates";
|
||||
import { concatUserSubmittedImagePrefix } from "~/utils/kysely.server";
|
||||
import { IMAGES_TO_VALIDATE_AT_ONCE } from "./upload-constants";
|
||||
import type { ImageUploadType } from "./upload-types";
|
||||
|
||||
/** Finds an unvalidated image by ID with associated calendar event data */
|
||||
export function findById(id: number) {
|
||||
@@ -96,39 +95,11 @@ export function unvalidatedImages() {
|
||||
.execute();
|
||||
}
|
||||
|
||||
/** Counts unvalidated team images submitted by a specific user */
|
||||
export async function countUnvalidatedBySubmitterUserId(userId: number) {
|
||||
const result = await db
|
||||
.selectFrom("UnvalidatedUserSubmittedImage")
|
||||
.innerJoin("Team", (join) =>
|
||||
join.on((eb) =>
|
||||
eb.or([
|
||||
eb(
|
||||
"UnvalidatedUserSubmittedImage.id",
|
||||
"=",
|
||||
eb.ref("Team.avatarImgId"),
|
||||
),
|
||||
eb(
|
||||
"UnvalidatedUserSubmittedImage.id",
|
||||
"=",
|
||||
eb.ref("Team.bannerImgId"),
|
||||
),
|
||||
]),
|
||||
),
|
||||
)
|
||||
.select(({ fn }) => fn.countAll<number>().as("count"))
|
||||
.where("UnvalidatedUserSubmittedImage.validatedAt", "is", null)
|
||||
.where("UnvalidatedUserSubmittedImage.submitterUserId", "=", userId)
|
||||
.executeTakeFirstOrThrow();
|
||||
return result.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts every unvalidated image submitted by a user, regardless of whether it is yet associated
|
||||
* with an entity. Unlike {@link countUnvalidatedBySubmitterUserId} (team-joined), this also counts
|
||||
* not-yet-connected orphans, so it can gate the SendouForm `image()` upload path.
|
||||
* Counts every unvalidated image submitted by a user, including not-yet-connected orphans, so it
|
||||
* can gate the SendouForm `image()` upload path.
|
||||
*/
|
||||
export async function countAllUnvalidatedBySubmitterUserId(userId: number) {
|
||||
export async function countUnvalidatedBySubmitterUserId(userId: number) {
|
||||
const result = await db
|
||||
.selectFrom("UnvalidatedUserSubmittedImage")
|
||||
.select(({ fn }) => fn.countAll<number>().as("count"))
|
||||
@@ -160,46 +131,3 @@ export function insert(
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
/** Creates a new image and associates it with a team or organization */
|
||||
export function addNewImage({
|
||||
submitterUserId,
|
||||
url,
|
||||
validatedAt,
|
||||
teamId,
|
||||
organizationId,
|
||||
type,
|
||||
}: {
|
||||
submitterUserId: number;
|
||||
url: string;
|
||||
validatedAt: number | null;
|
||||
teamId?: number;
|
||||
organizationId?: number;
|
||||
type: ImageUploadType;
|
||||
}) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const img = await insert({ submitterUserId, url, validatedAt }, trx);
|
||||
|
||||
if (type === "team-pfp" && teamId) {
|
||||
await trx
|
||||
.updateTable("AllTeam")
|
||||
.set({ avatarImgId: img.id })
|
||||
.where("id", "=", teamId)
|
||||
.execute();
|
||||
} else if (type === "team-banner" && teamId) {
|
||||
await trx
|
||||
.updateTable("AllTeam")
|
||||
.set({ bannerImgId: img.id })
|
||||
.where("id", "=", teamId)
|
||||
.execute();
|
||||
} else if (type === "org-pfp" && organizationId) {
|
||||
await trx
|
||||
.updateTable("TournamentOrganization")
|
||||
.set({ avatarImgId: img.id })
|
||||
.where("id", "=", organizationId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
return img;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import type { FileUpload } from "@remix-run/form-data-parser";
|
||||
import type { ActionFunctionArgs } from "react-router";
|
||||
import { redirect } from "react-router";
|
||||
import { z } from "zod";
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import * as TeamRepository from "~/features/team/TeamRepository.server";
|
||||
import { isTeamManager } from "~/features/team/team-utils";
|
||||
import * as TournamentOrganizationRepository from "~/features/tournament-organization/TournamentOrganizationRepository.server";
|
||||
import { requirePermission } from "~/modules/permissions/guards.server";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import invariant from "~/utils/invariant";
|
||||
import {
|
||||
badRequestIfFalsy,
|
||||
errorToastIfFalsy,
|
||||
parseSearchParams,
|
||||
safeParseMultipartFormData,
|
||||
} from "~/utils/remix.server";
|
||||
import { teamPage, tournamentOrganizationPage } from "~/utils/urls";
|
||||
import * as ImageRepository from "../ImageRepository.server";
|
||||
import { uploadStreamToS3 } from "../s3.server";
|
||||
import {
|
||||
ALLOWED_IMAGE_EXTENSIONS,
|
||||
MAX_UNVALIDATED_IMG_COUNT,
|
||||
} from "../upload-constants";
|
||||
import { requestToImgType } from "../upload-utils";
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const user = requireUser();
|
||||
|
||||
const validatedType = requestToImgType(request);
|
||||
errorToastIfFalsy(validatedType, "Invalid image type");
|
||||
|
||||
const team =
|
||||
validatedType === "team-pfp" || validatedType === "team-banner"
|
||||
? await validatedTeam({ user, request })
|
||||
: undefined;
|
||||
const organization =
|
||||
validatedType === "org-pfp"
|
||||
? await requireEditableOrganization(request)
|
||||
: undefined;
|
||||
|
||||
errorToastIfFalsy(
|
||||
(await ImageRepository.countUnvalidatedBySubmitterUserId(user.id)) <
|
||||
MAX_UNVALIDATED_IMG_COUNT,
|
||||
"Too many unvalidated images",
|
||||
);
|
||||
|
||||
const uploadHandler = async (fileUpload: FileUpload) => {
|
||||
if (fileUpload.fieldName === "img") {
|
||||
const ending = fileUpload.name.split(".").pop()?.toLowerCase();
|
||||
invariant(ending && ending !== fileUpload.name);
|
||||
invariant(
|
||||
ALLOWED_IMAGE_EXTENSIONS.includes(ending),
|
||||
`Invalid file extension: "${ending}"`,
|
||||
);
|
||||
const newFilename = `img-${Date.now()}.${ending}`;
|
||||
|
||||
const uploadedFileLocation = await uploadStreamToS3(
|
||||
fileUpload.stream(),
|
||||
newFilename,
|
||||
);
|
||||
return uploadedFileLocation;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const formData = await safeParseMultipartFormData(request, uploadHandler);
|
||||
const imgSrc = formData.get("img") as string | null;
|
||||
invariant(imgSrc);
|
||||
|
||||
const urlParts = imgSrc.split("/");
|
||||
const fileName = urlParts[urlParts.length - 1];
|
||||
invariant(fileName);
|
||||
|
||||
const shouldAutoValidate =
|
||||
user.roles.includes("SUPPORTER") || validatedType === "org-pfp";
|
||||
|
||||
await ImageRepository.addNewImage({
|
||||
submitterUserId: user.id,
|
||||
teamId: team?.id,
|
||||
organizationId: organization?.id,
|
||||
type: validatedType,
|
||||
url: fileName,
|
||||
validatedAt: shouldAutoValidate
|
||||
? dateToDatabaseTimestamp(new Date())
|
||||
: null,
|
||||
});
|
||||
|
||||
if (shouldAutoValidate) {
|
||||
if (team) {
|
||||
throw redirect(teamPage(team?.customUrl));
|
||||
}
|
||||
if (organization) {
|
||||
throw redirect(
|
||||
tournamentOrganizationPage({ organizationSlug: organization.slug }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
async function validatedTeam({
|
||||
user,
|
||||
request,
|
||||
}: {
|
||||
user: { id: number };
|
||||
request: Request;
|
||||
}) {
|
||||
const { team: teamCustomUrl } = parseSearchParams({
|
||||
request,
|
||||
schema: z.object({ team: z.string() }),
|
||||
});
|
||||
const team = await TeamRepository.findByCustomUrl(teamCustomUrl);
|
||||
|
||||
errorToastIfFalsy(team, "Team not found");
|
||||
errorToastIfFalsy(
|
||||
isTeamManager({ team, user }),
|
||||
"You must be the team manager to upload images",
|
||||
);
|
||||
|
||||
return team;
|
||||
}
|
||||
|
||||
async function requireEditableOrganization(request: Request) {
|
||||
const { slug } = parseSearchParams({
|
||||
request,
|
||||
schema: z.object({ slug: z.string() }),
|
||||
});
|
||||
const organization = badRequestIfFalsy(
|
||||
await TournamentOrganizationRepository.findBySlug(slug),
|
||||
);
|
||||
|
||||
requirePermission(organization, "EDIT");
|
||||
|
||||
return organization;
|
||||
}
|
||||
@@ -16,22 +16,26 @@ import { MAX_UNVALIDATED_IMG_COUNT } from "./upload-constants";
|
||||
* - `null` → `null` (image removed / none)
|
||||
* - `EXISTING` → the unchanged `imgId` (no bytes are re-uploaded)
|
||||
* - `NEW` → decodes the base64 webp, uploads it to S3 and inserts an unvalidated image row,
|
||||
* auto-validating it for supporters, then returns the new id.
|
||||
* auto-validating it for supporters (or always when `autoValidate` is set), then returns the
|
||||
* new id.
|
||||
*
|
||||
* The consuming action is responsible for writing the returned value to its own FK column.
|
||||
*/
|
||||
export async function imageFieldValueToImgId({
|
||||
value,
|
||||
user,
|
||||
autoValidate = false,
|
||||
}: {
|
||||
value: ImageFieldValue;
|
||||
user: AuthenticatedUser;
|
||||
/** Validate the image immediately, bypassing the moderator queue (e.g. trusted org logos). */
|
||||
autoValidate?: boolean;
|
||||
}): Promise<number | null> {
|
||||
if (!value) return null;
|
||||
if (value.type === "EXISTING") return value.imgId;
|
||||
|
||||
errorToastIfFalsy(
|
||||
(await ImageRepository.countAllUnvalidatedBySubmitterUserId(user.id)) <
|
||||
(await ImageRepository.countUnvalidatedBySubmitterUserId(user.id)) <
|
||||
MAX_UNVALIDATED_IMG_COUNT,
|
||||
"Too many unvalidated images",
|
||||
);
|
||||
@@ -45,7 +49,7 @@ export async function imageFieldValueToImgId({
|
||||
invariant(uploadedFileLocation, "Image upload failed");
|
||||
const fileName = basename(uploadedFileLocation);
|
||||
|
||||
const shouldAutoValidate = user.roles.includes("SUPPORTER");
|
||||
const shouldAutoValidate = autoValidate || user.roles.includes("SUPPORTER");
|
||||
|
||||
const img = await ImageRepository.insert({
|
||||
submitterUserId: user.id,
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { LoaderFunctionArgs } from "react-router";
|
||||
import { redirect } from "react-router";
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import * as TeamRepository from "~/features/team/TeamRepository.server";
|
||||
import { isTeamManager } from "~/features/team/team-utils";
|
||||
import * as ImageRepository from "../ImageRepository.server";
|
||||
import { requestToImgType } from "../upload-utils";
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const user = requireUser();
|
||||
const validatedType = requestToImgType(request);
|
||||
|
||||
if (!validatedType) {
|
||||
throw redirect("/");
|
||||
}
|
||||
|
||||
if (validatedType === "team-pfp" || validatedType === "team-banner") {
|
||||
const teamCustomUrl = new URL(request.url).searchParams.get("team") ?? "";
|
||||
const team = await TeamRepository.findByCustomUrl(teamCustomUrl);
|
||||
|
||||
if (!team || !isTeamManager({ team, user })) {
|
||||
throw redirect("/");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: validatedType,
|
||||
unvalidatedImages: await ImageRepository.countUnvalidatedBySubmitterUserId(
|
||||
user.id,
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -1,117 +0,0 @@
|
||||
import Compressor from "compressorjs";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFetcher, useLoaderData } from "react-router";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { Main } from "~/components/Main";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { logger } from "~/utils/logger";
|
||||
import { action } from "../actions/upload.server";
|
||||
import { loader } from "../loaders/upload.server";
|
||||
import { imgTypeToDimensions, imgTypeToStyle } from "../upload-constants";
|
||||
import type { ImageUploadType } from "../upload-types";
|
||||
|
||||
export { action, loader };
|
||||
|
||||
export default function FileUploadPage() {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [img, setImg] = React.useState<File | null>(null);
|
||||
const fetcher = useFetcher();
|
||||
|
||||
const handleSubmit = () => {
|
||||
invariant(img);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("img", img, img.name);
|
||||
|
||||
fetcher.submit(formData, {
|
||||
encType: "multipart/form-data",
|
||||
method: "post",
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (fetcher.state === "loading") {
|
||||
setImg(null);
|
||||
}
|
||||
}, [fetcher.state]);
|
||||
|
||||
const { width, height } = imgTypeToDimensions[data.type];
|
||||
|
||||
return (
|
||||
<Main className="stack lg">
|
||||
<div>
|
||||
<div>
|
||||
{t("common:upload.title", {
|
||||
type: t(`common:upload.type.${data.type}`),
|
||||
width,
|
||||
height,
|
||||
})}
|
||||
</div>
|
||||
{data.type === "team-banner" || data.type === "team-pfp" ? (
|
||||
<div className="text-sm text-lighter">
|
||||
{t("common:upload.commonExplanation")}{" "}
|
||||
{data.unvalidatedImages ? (
|
||||
<span>
|
||||
{t("common:upload.afterExplanation", {
|
||||
count: data.unvalidatedImages,
|
||||
})}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="img-field">{t("common:upload.imageToUpload")}</label>
|
||||
<input
|
||||
id="img-field"
|
||||
type="file"
|
||||
name="img"
|
||||
accept="image/png, image/jpeg, image/webp"
|
||||
onChange={(e) => {
|
||||
const uploadedFile = e.target.files?.[0];
|
||||
if (!uploadedFile) {
|
||||
setImg(null);
|
||||
return;
|
||||
}
|
||||
|
||||
new Compressor(uploadedFile, {
|
||||
height,
|
||||
width,
|
||||
maxHeight: height,
|
||||
maxWidth: width,
|
||||
// 0.5MB
|
||||
convertSize: 500_000,
|
||||
resize: "cover",
|
||||
success(result) {
|
||||
const file = new File([result], "img.webp", {
|
||||
type: "image/webp",
|
||||
});
|
||||
setImg(file);
|
||||
},
|
||||
error(err) {
|
||||
logger.error(err.message);
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{img ? <PreviewImage img={img} type={data.type} /> : null}
|
||||
<SendouButton
|
||||
className="self-start"
|
||||
isDisabled={!img || fetcher.state !== "idle"}
|
||||
onPress={handleSubmit}
|
||||
data-testid="upload-button"
|
||||
>
|
||||
{t("common:actions.upload")}
|
||||
</SendouButton>
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviewImage({ img, type }: { img: File; type: ImageUploadType }) {
|
||||
return (
|
||||
<img src={URL.createObjectURL(img)} alt="" style={imgTypeToStyle[type]} />
|
||||
);
|
||||
}
|
||||
@@ -6,19 +6,12 @@ export const MAX_UNVALIDATED_IMG_COUNT = 5;
|
||||
|
||||
export const IMAGES_TO_VALIDATE_AT_ONCE = 5;
|
||||
|
||||
export const IMAGE_TYPES = ["team-pfp", "org-pfp", "team-banner"] as const;
|
||||
export const IMAGE_TYPES = ["team-pfp", "team-banner"] as const;
|
||||
|
||||
export const imgTypeToDimensions: Record<
|
||||
ImageUploadType,
|
||||
{ width: number; height: number }
|
||||
> = {
|
||||
"team-pfp": { width: 400, height: 400 },
|
||||
"org-pfp": { width: 400, height: 400 },
|
||||
"team-banner": { width: 1000, height: 500 },
|
||||
};
|
||||
|
||||
export const imgTypeToStyle: Record<ImageUploadType, React.CSSProperties> = {
|
||||
"team-pfp": { borderRadius: "100%", width: "144px", height: "144px" },
|
||||
"org-pfp": { borderRadius: "100%", width: "144px", height: "144px" },
|
||||
"team-banner": { borderRadius: "var(--radius-box)", width: "100%" },
|
||||
};
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { IMAGE_TYPES } from "./upload-constants";
|
||||
|
||||
export function requestToImgType(request: Request) {
|
||||
const rawType = new URL(request.url).searchParams.get("type") ?? "";
|
||||
return IMAGE_TYPES.find((type) => type === rawType);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { CustomThemeSelector } from "~/components/CustomThemeSelector";
|
||||
import { Divider } from "~/components/Divider";
|
||||
import { Main, mainStyles } from "~/components/Main";
|
||||
import { TeamGoBackButton } from "~/features/team/components/TeamGoBackButton";
|
||||
import { existingImage } from "~/form/image-field";
|
||||
import { SendouForm } from "~/form/SendouForm";
|
||||
import type { ThemeInput } from "~/utils/oklch-gamut";
|
||||
import { metaTags } from "~/utils/remix";
|
||||
@@ -38,22 +39,8 @@ export default function EditTeamPage() {
|
||||
tag: team.tag ?? "",
|
||||
bsky: team.bsky ?? "",
|
||||
bio: team.bio ?? "",
|
||||
logo:
|
||||
team.avatarImgId && team.avatarUrl
|
||||
? {
|
||||
type: "EXISTING",
|
||||
imgId: team.avatarImgId,
|
||||
url: team.avatarUrl,
|
||||
}
|
||||
: null,
|
||||
banner:
|
||||
team.bannerImgId && team.bannerUrl
|
||||
? {
|
||||
type: "EXISTING",
|
||||
imgId: team.bannerImgId,
|
||||
url: team.bannerUrl,
|
||||
}
|
||||
: null,
|
||||
logo: existingImage(team.avatarImgId, team.avatarUrl),
|
||||
banner: existingImage(team.bannerImgId, team.bannerUrl),
|
||||
}}
|
||||
submitButtonText={t("common:actions.submit")}
|
||||
submitButtonTestId="edit-team-submit-button"
|
||||
|
||||
@@ -65,6 +65,7 @@ export async function findBySlug(slug: string) {
|
||||
"TournamentOrganization.socials",
|
||||
"TournamentOrganization.slug",
|
||||
"TournamentOrganization.isEstablished",
|
||||
"TournamentOrganization.avatarImgId",
|
||||
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
|
||||
"avatarUrl",
|
||||
),
|
||||
@@ -411,6 +412,8 @@ interface UpdateArgs
|
||||
Tables["TournamentOrganization"],
|
||||
"id" | "name" | "description" | "socials"
|
||||
> {
|
||||
/** Omit to leave the current logo unchanged; `null` clears it. */
|
||||
avatarImgId?: number | null;
|
||||
members: Array<
|
||||
Pick<
|
||||
Tables["TournamentOrganizationMember"],
|
||||
@@ -430,11 +433,29 @@ export function update({
|
||||
name,
|
||||
description,
|
||||
socials,
|
||||
avatarImgId,
|
||||
members,
|
||||
series,
|
||||
badges,
|
||||
}: UpdateArgs) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
if (avatarImgId !== undefined) {
|
||||
const current = await trx
|
||||
.selectFrom("TournamentOrganization")
|
||||
.select("avatarImgId")
|
||||
.where("id", "=", id)
|
||||
.executeTakeFirst();
|
||||
|
||||
// the logo got removed or replaced, so the old submitted image row is
|
||||
// no longer referenced by anything and is cleaned up
|
||||
if (current?.avatarImgId && current.avatarImgId !== avatarImgId) {
|
||||
await trx
|
||||
.deleteFrom("UnvalidatedUserSubmittedImage")
|
||||
.where("id", "=", current.avatarImgId)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
const updatedOrg = await trx
|
||||
.updateTable("TournamentOrganization")
|
||||
.set({
|
||||
@@ -442,6 +463,7 @@ export function update({
|
||||
description,
|
||||
slug: mySlugify(name),
|
||||
socials: socials ? JSON.stringify(socials) : null,
|
||||
...(avatarImgId !== undefined ? { avatarImgId } : {}),
|
||||
})
|
||||
.where("id", "=", id)
|
||||
.returningAll()
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type ActionFunctionArgs, redirect } from "react-router";
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
|
||||
import { clearTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import { parseFormData } from "~/form/parse.server";
|
||||
import { parseFormDataWithImages } from "~/form/parse.server";
|
||||
import { i18next } from "~/modules/i18n/i18next.server";
|
||||
import { requirePermission } from "~/modules/permissions/guards.server";
|
||||
import { actionError } from "~/utils/remix.server";
|
||||
@@ -13,9 +13,10 @@ import { organizationFromParams } from "../tournament-organization-utils.server"
|
||||
|
||||
export const action = async ({ request, params }: ActionFunctionArgs) => {
|
||||
const user = requireUser();
|
||||
const result = await parseFormData({
|
||||
const result = await parseFormDataWithImages({
|
||||
request,
|
||||
schema: organizationEditFormSchema,
|
||||
autoValidate: true,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
@@ -48,6 +49,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
socials: socials.length > 0 ? socials : null,
|
||||
avatarImgId: data.logo,
|
||||
members: data.members,
|
||||
series: data.series,
|
||||
badges: data.badges,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useLoaderData } from "react-router";
|
||||
import { useLoaderData } from "react-router";
|
||||
import { Main } from "~/components/Main";
|
||||
import { existingImage } from "~/form/image-field";
|
||||
import { SendouForm } from "~/form/SendouForm";
|
||||
import { uploadImagePage } from "~/utils/urls";
|
||||
import { action } from "../actions/org.$slug.edit.server";
|
||||
import { loader } from "../loaders/org.$slug.edit.server";
|
||||
import { handle, meta } from "../routes/org.$slug";
|
||||
@@ -21,6 +21,10 @@ export default function TournamentOrganizationEditPage() {
|
||||
schema={organizationEditFormSchema}
|
||||
defaultValues={{
|
||||
name: data.organization.name,
|
||||
logo: existingImage(
|
||||
data.organization.avatarImgId,
|
||||
data.organization.avatarUrl,
|
||||
),
|
||||
description: data.organization.description ?? "",
|
||||
socials: data.organization.socials ?? [],
|
||||
members: data.organization.members.map((member) => ({
|
||||
@@ -38,17 +42,8 @@ export default function TournamentOrganizationEditPage() {
|
||||
>
|
||||
{({ FormField }) => (
|
||||
<>
|
||||
<Link
|
||||
to={uploadImagePage({
|
||||
type: "org-pfp",
|
||||
slug: data.organization.slug,
|
||||
})}
|
||||
className="text-sm font-bold"
|
||||
>
|
||||
{t("org:edit.form.uploadLogo")}
|
||||
</Link>
|
||||
|
||||
<FormField name="name" />
|
||||
<FormField name="logo" />
|
||||
<FormField name="description" />
|
||||
<FormField name="members" />
|
||||
<FormField name="socials" />
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
badges,
|
||||
datetimeOptional,
|
||||
fieldset,
|
||||
image,
|
||||
select,
|
||||
stringConstant,
|
||||
textAreaOptional,
|
||||
@@ -33,6 +34,7 @@ export const newOrganizationSchema = z.object({
|
||||
|
||||
export const organizationEditFormSchema = z.object({
|
||||
name: orgNameField,
|
||||
logo: image({ label: "labels.logo" }),
|
||||
description: textAreaOptional({
|
||||
label: "labels.description",
|
||||
maxLength: TOURNAMENT_ORGANIZATION.DESCRIPTION_MAX_LENGTH,
|
||||
|
||||
@@ -35,6 +35,17 @@ export const imageValue = z
|
||||
|
||||
export type ImageFieldValue = z.infer<typeof imageValue>;
|
||||
|
||||
/**
|
||||
* Builds an `EXISTING` {@link ImageFieldValue} for an edit form's default values, or `null`
|
||||
* when either the id or preview url is missing.
|
||||
*/
|
||||
export function existingImage(
|
||||
imgId: number | null | undefined,
|
||||
url: string | null | undefined,
|
||||
): ImageFieldValue {
|
||||
return imgId && url ? { type: "EXISTING", imgId, url } : null;
|
||||
}
|
||||
|
||||
export type ImageFieldDimensions =
|
||||
| "logo"
|
||||
| "thick-banner"
|
||||
|
||||
@@ -58,9 +58,12 @@ type ResolvedImages<T> = T extends unknown
|
||||
export async function parseFormDataWithImages<T extends z.ZodTypeAny>({
|
||||
request,
|
||||
schema,
|
||||
autoValidate = false,
|
||||
}: {
|
||||
request: Request;
|
||||
schema: T;
|
||||
/** Validate uploaded images immediately, bypassing the moderator queue (e.g. trusted org logos). */
|
||||
autoValidate?: boolean;
|
||||
}): Promise<ParseResult<ResolvedImages<z.infer<T>>>> {
|
||||
const result = await parseFormData({ request, schema });
|
||||
if (!result.success) return result;
|
||||
@@ -73,6 +76,7 @@ export async function parseFormDataWithImages<T extends z.ZodTypeAny>({
|
||||
data[key] = await imageFieldValueToImgId({
|
||||
value: data[key] as ImageFieldValue,
|
||||
user,
|
||||
autoValidate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,6 @@ export default [
|
||||
|
||||
route("/maps", "features/map-list-generator/routes/maps.tsx"),
|
||||
|
||||
route("/upload", "features/img-upload/routes/upload.tsx"),
|
||||
route("/upload/admin", "features/img-upload/routes/upload.admin.tsx"),
|
||||
|
||||
route("/plans", "features/map-planner/routes/plans.tsx"),
|
||||
|
||||
@@ -434,15 +434,6 @@ export const objectDamageCalculatorPage = (weaponId?: MainWeaponId) =>
|
||||
typeof weaponId === "number" ? `?weapon=${weaponId}` : ""
|
||||
}`;
|
||||
|
||||
export const uploadImagePage = (
|
||||
args:
|
||||
| { type: "team-pfp" | "team-banner"; teamCustomUrl: string }
|
||||
| { type: "org-pfp"; slug: string },
|
||||
) =>
|
||||
args.type === "org-pfp"
|
||||
? `/upload?type=${args.type}&slug=${args.slug}`
|
||||
: `/upload?type=${args.type}&team=${args.teamCustomUrl}`;
|
||||
|
||||
export const vodVideoPage = (videoId: number) => `${VODS_PAGE}/${videoId}`;
|
||||
|
||||
export const lfgNewPostPage = (postId?: number) =>
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "Bliv medlem",
|
||||
"actions.nevermind": "",
|
||||
"actions.upload": "Upload",
|
||||
"actions.clickHere": "",
|
||||
"actions.goBack": "",
|
||||
"actions.enable": "",
|
||||
@@ -236,13 +235,6 @@
|
||||
"theme.auto": "Automatisk",
|
||||
"websiteSubtitle": "Konkurrencepræget Splatoon-hub",
|
||||
"upload.imageToUpload": "Valgte billede",
|
||||
"upload.title": "Uploader {{type}}. Den anbefalede opløsning er {{width}}×{{height}}.",
|
||||
"upload.type.team-pfp": "Holdprofilbillede",
|
||||
"upload.type.team-banner": "profilbillede til et hold ",
|
||||
"upload.type.org-pfp": "",
|
||||
"upload.commonExplanation": "Billedet vil først blive offentligt tilgængeligt, når en moderator har godkendt billedet. Billeder, der er uploadet af Patreons bliver fremvist uden moderatorgodkendelse.",
|
||||
"upload.afterExplanation_one": "Du har {{count}} billede, der afventer moderatorgodkendelse. Billedet vil blive vist automatisk efter det er blevet godkendt.",
|
||||
"upload.afterExplanation_other": "Du har {{count}} billeder, der afventer moderatorgodkendelse. Billederne vil blive vist automatisk efter de er blevet godkendt.",
|
||||
"support.intro.first": "Hej! Mit navn er Sendou og sendou.ink er mit projekt, hvis formål er at stille redskaber og ressourcer til rådighed for Splatoon-fællesskabet. Målet er at hjælpe alle med at nyde og blive bedre til Splatoon. Hvad enten om du lige er startet eller har spillet Splatoon i mange timer.",
|
||||
"support.intro.second": "Hvis du kan lide, hvad jeg laver på denne hjemmeside, så du støtte mit værk. Denne side beskriver, hvordan du kan støtte mit arbejde og opnå frynsegoder på sendou.ink. Din støtte hjælper mig med at betale for at hoste hjemmesiden, samt sponsorere den tid, som jeg fortsat bruger på at forbedre hjemmesiden.",
|
||||
"support.action": "Støt via Patreon",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "",
|
||||
"events.tabs.leaderboard": "",
|
||||
"edit.form.title": "",
|
||||
"edit.form.uploadLogo": "",
|
||||
"edit.form.socialLinks.title": "",
|
||||
"edit.form.members.title": "",
|
||||
"edit.form.members.info": "",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "Beitreten",
|
||||
"actions.nevermind": "",
|
||||
"actions.upload": "Hochladen",
|
||||
"actions.clickHere": "",
|
||||
"actions.goBack": "",
|
||||
"actions.enable": "",
|
||||
@@ -236,13 +235,6 @@
|
||||
"theme.auto": "Auto",
|
||||
"websiteSubtitle": "Hub für Competitive Splatoon",
|
||||
"upload.imageToUpload": "Bild hochladen",
|
||||
"upload.title": "{{type}} wird hochgeladen. Empfohlene Größe: {{width}}×{{height}}.",
|
||||
"upload.type.team-pfp": "Team-Profilbild",
|
||||
"upload.type.team-banner": "Team-Banner",
|
||||
"upload.type.org-pfp": "",
|
||||
"upload.commonExplanation": "Bevor das Bild veröffentlicht wird, überprüft es ein Moderator. Bilder von Patrons werden ohne Überprüfung veröffentlicht.",
|
||||
"upload.afterExplanation_one": "{{count}} Bild ist ausstehend. Das Bild wird nach der Überprüfung automatisch veröffentlicht.",
|
||||
"upload.afterExplanation_other": "{{count}} Bilder sind ausstehend. Die Bilder werden nach der Überprüfung automatisch veröffentlicht.",
|
||||
"support.intro.first": "Hallo! Ich bin Sendou und sendou.ink ist mein Projekt, um Tools und Ressourcen für Splatoon-Community bereitzustellen. Das Ziel ist es, jedem zu helfen, sich zu verbessern und Splatoon zu genießen - egal ob man ganz neu im Spiel ist, oder ein erfahrener Veteran.",
|
||||
"support.intro.second": "Wenn dir die Seite gefällt und sie unterstützen willst und Vorteile erhälten möchtest, findest du auf dieser Seite dazu Details. Dein Support hilft mir, die Serverkosten zu bezahlen und ermöglicht mir Zeit in das Projekt zu investieren, um es ständig zu verbessern.",
|
||||
"support.action": "Auf Patreon unterstützen",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "",
|
||||
"events.tabs.leaderboard": "",
|
||||
"edit.form.title": "",
|
||||
"edit.form.uploadLogo": "",
|
||||
"edit.form.socialLinks.title": "",
|
||||
"edit.form.members.title": "",
|
||||
"edit.form.members.info": "",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "No outline",
|
||||
"actions.join": "Join",
|
||||
"actions.nevermind": "Nevermind",
|
||||
"actions.upload": "Upload",
|
||||
"actions.clickHere": "Click here",
|
||||
"actions.goBack": "Go back",
|
||||
"actions.enable": "Enable",
|
||||
@@ -236,13 +235,6 @@
|
||||
"theme.auto": "Auto",
|
||||
"websiteSubtitle": "Competitive Splatoon Hub",
|
||||
"upload.imageToUpload": "Image to upload",
|
||||
"upload.title": "Uploading {{type}}. Recommended size is {{width}}×{{height}}.",
|
||||
"upload.type.team-pfp": "team profile picture",
|
||||
"upload.type.team-banner": "team picture banner",
|
||||
"upload.type.org-pfp": "tournament organization profile picture",
|
||||
"upload.commonExplanation": "Before the image is publicly displayed a moderator will validate it. Images uploaded by patrons are shown without validation.",
|
||||
"upload.afterExplanation_one": "You have {{count}} image pending. The image will show up automatically after validation.",
|
||||
"upload.afterExplanation_other": "You have {{count}} images pending. The images will show up automatically after validation.",
|
||||
"support.intro.first": "Hello! I'm Sendou and sendou.ink is my project to provide tools and resources for the Splatoon community. The goal is to help everyone to improve and enjoy Splatoon whether you are brand new to the game or a seasoned veteran.",
|
||||
"support.intro.second": "If you like what I'm doing this page details how you can support my work and gain perks. Your support helps me pay for the hosting as well as sponsor my time spent on continuously improving the project.",
|
||||
"support.action": "Support on Patreon",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "Events",
|
||||
"events.tabs.leaderboard": "Leaderboard",
|
||||
"edit.form.title": "Editing tournament organization",
|
||||
"edit.form.uploadLogo": "Upload logo",
|
||||
"edit.form.socialLinks.title": "Social links",
|
||||
"edit.form.members.title": "Members",
|
||||
"edit.form.members.info": "\"Admin\" role lets users edit the organization and any tournament registration hosted by the organization. \"Organizer\" and \"Streamer\" give the corresponding role in tournaments. \"Member\" has no special permissions attached to it.",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "Sin borde",
|
||||
"actions.join": "Unirse",
|
||||
"actions.nevermind": "Olvídalo",
|
||||
"actions.upload": "Subir",
|
||||
"actions.clickHere": "Haz clic aquí",
|
||||
"actions.goBack": "Volver atrás",
|
||||
"actions.enable": "Activar",
|
||||
@@ -237,14 +236,6 @@
|
||||
"theme.auto": "Automático",
|
||||
"websiteSubtitle": "Centro Competitivo de Splatoon",
|
||||
"upload.imageToUpload": "Imagen a subir",
|
||||
"upload.title": "Subiendo {{type}}. El tamaño recomendado es {{width}}×{{height}}.",
|
||||
"upload.type.team-pfp": "foto de perfil del equipo",
|
||||
"upload.type.team-banner": "banner del equipo",
|
||||
"upload.type.org-pfp": "foto de perfil de la organización",
|
||||
"upload.commonExplanation": "Antes de que la imagen sea pública, un moderador debe aprobarla. Las imágenes subidas por mecenas (Patreon) se muestran sin validación.",
|
||||
"upload.afterExplanation_one": "Tienes {{count}} imagen pendiente. La imagen aparecerá automáticamente tras ser aprobada.",
|
||||
"upload.afterExplanation_many": "Tienes {{count}} imágenes pendientes. Las imágenes aparecerán automáticamente tras ser aprobadas.",
|
||||
"upload.afterExplanation_other": "Tienes {{count}} imágenes pendientes. Las imágenes aparecerán automáticamente tras ser aprobadas.",
|
||||
"support.intro.first": "¡Hola! Soy Sendou y sendou.ink es mi proyecto para proporcionar herramientas y recursos a la comunidad de Splatoon. El objetivo es ayudar a todos a mejorar y disfrutar de Splatoon, ya sean principiantes o veteranos.",
|
||||
"support.intro.second": "Si te gusta lo que hago, esta página detalla cómo puedes apoyar mi trabajo y obtener ventajas. Tu apoyo me ayuda a pagar el alojamiento y me permite dedicar tiempo a mejorar continuamente el proyecto.",
|
||||
"support.action": "Apoyar en Patreon",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "Eventos",
|
||||
"events.tabs.leaderboard": "Tablas de posición",
|
||||
"edit.form.title": "Editando organización de torneos",
|
||||
"edit.form.uploadLogo": "Subir logo",
|
||||
"edit.form.socialLinks.title": "Enlaces sociales",
|
||||
"edit.form.members.title": "Miembros",
|
||||
"edit.form.members.info": "El rol \"Admin\" permite a los usuarios editar la organización y cualquier registro de torneo albergado por la organización. Los roles \"Organizador\" y \"Streamer\" dan el rol correspondiente en los torneos. El rol de \"Miembro\" no tiene ningún permiso especial asociado.",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "Unirse",
|
||||
"actions.nevermind": "Cancelar",
|
||||
"actions.upload": "Subir",
|
||||
"actions.clickHere": "Haga click aquí",
|
||||
"actions.goBack": "Volver",
|
||||
"actions.enable": "",
|
||||
@@ -237,14 +236,6 @@
|
||||
"theme.auto": "Auto",
|
||||
"websiteSubtitle": "Sitio Central de Splatoon Competitivo",
|
||||
"upload.imageToUpload": "Imagen para subir",
|
||||
"upload.title": "Subiendo {{type}}. Se recomienda tamaño de {{width}}×{{height}}.",
|
||||
"upload.type.team-pfp": "imagen de equipo",
|
||||
"upload.type.team-banner": "imagen bandera de equipo",
|
||||
"upload.type.org-pfp": "imagen de perfil de organización de torneos",
|
||||
"upload.commonExplanation": "Antes de que la imagen sea publica, debe ser aprobada por un moderador. Imagenes subidas por apoyantes de Patreon no requieren ser aprobadas. ",
|
||||
"upload.afterExplanation_one": "Tienes {{count}} imagen pendiente. La imagen se auto-publica despues de ser aprobada.",
|
||||
"upload.afterExplanation_many": "",
|
||||
"upload.afterExplanation_other": "Tienes {{count}} imagenes pendientes. Las imagenes se auto-publican despues de ser aprobadas.",
|
||||
"support.intro.first": "¡Hola! Soy Sendou y sendou.ink es mi proyecto para proveer herramientas y recursos para la comunidad de Splatoon. La meta es ayudar a todos a mejorar y disfrutar Splatoon ya sean principiantes o veteranos.",
|
||||
"support.intro.second": "Si te gusta lo que hago, esta página detalla como puedes dar tu apoyo a mi trabajo y recibir beneficios. Tu apoyo me ayuda pagar el costo del sitio y me hace habil pasar tiempo mejorando el proyecto.",
|
||||
"support.action": "Apoyanos en Patreon",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "Eventos",
|
||||
"events.tabs.leaderboard": "Tablas de posición",
|
||||
"edit.form.title": "Editando organización de torneos",
|
||||
"edit.form.uploadLogo": "Subir logo",
|
||||
"edit.form.socialLinks.title": "Enlaces sociales",
|
||||
"edit.form.members.title": "Miembros",
|
||||
"edit.form.members.info": "El rol \"Admin\" le permite usuarios editar la organización y cualquier registro de torneo albergado por la organización. Los roles \"Organizador\" y \"Streamer\" den el rol correspondiente en los torneos. El rol de \"Miembro\" no tiene ningún permiso especial asociado.",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "Joindre",
|
||||
"actions.nevermind": "",
|
||||
"actions.upload": "Soumettre",
|
||||
"actions.clickHere": "",
|
||||
"actions.goBack": "",
|
||||
"actions.enable": "",
|
||||
@@ -237,14 +236,6 @@
|
||||
"theme.auto": "Auto",
|
||||
"websiteSubtitle": "Le hub compétitif de Splatoon",
|
||||
"upload.imageToUpload": "Image à soumettre",
|
||||
"upload.title": "Soumission {{type}}. La taille recommandée est de {{width}}×{{height}}.",
|
||||
"upload.type.team-pfp": "d'un emblème d'équipe",
|
||||
"upload.type.team-banner": "d'une bannière d'équipe",
|
||||
"upload.type.org-pfp": "",
|
||||
"upload.commonExplanation": "Avant que l'image ne soit visible publiquement, un modérateur la validera. Les images soumises par les Supporters n'ont pas besoin d'être validées.",
|
||||
"upload.afterExplanation_one": "Vous avez {{count}} image en attente. L'image sera visible une fois validée.",
|
||||
"upload.afterExplanation_many": "",
|
||||
"upload.afterExplanation_other": "Vous avez {{count}} images en attente. Les images seront visibles une fois validées.",
|
||||
"support.intro.first": "Salut! Je suis Sendou et sendou.ink est mon projet pour fournir des outils et ressources pour la communauté Splatoon. L'objectif est d'aider tout le monde à s'améliorer et à profiter de Splatoon, que vous soyez tout nouveau dans le jeu ou un vétéran chevronné.",
|
||||
"support.intro.second": "Si vous aimez ce que je fais, cette page détaille comment vous pouvez soutenir mon travail et gagner des avantages. Votre soutien m'aide à payer l'hébergement ainsi qu'à sponsoriser mon temps passé à améliorer continuellement le projet.",
|
||||
"support.action": "Soutenir sur Patreon",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "",
|
||||
"events.tabs.leaderboard": "",
|
||||
"edit.form.title": "",
|
||||
"edit.form.uploadLogo": "",
|
||||
"edit.form.socialLinks.title": "",
|
||||
"edit.form.members.title": "",
|
||||
"edit.form.members.info": "",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "No outline",
|
||||
"actions.join": "Joindre",
|
||||
"actions.nevermind": "Laisser tomber",
|
||||
"actions.upload": "Soumettre",
|
||||
"actions.clickHere": "Click ici",
|
||||
"actions.goBack": "Retourner en arrière",
|
||||
"actions.enable": "Activer",
|
||||
@@ -237,14 +236,6 @@
|
||||
"theme.auto": "Auto",
|
||||
"websiteSubtitle": "Le hub compétitif de Splatoon",
|
||||
"upload.imageToUpload": "Image à soumettre",
|
||||
"upload.title": "Soumission {{type}}. La taille recommandée est de {{width}}×{{height}}.",
|
||||
"upload.type.team-pfp": "d'un emblème d'équipe",
|
||||
"upload.type.team-banner": "d'une bannière d'équipe",
|
||||
"upload.type.org-pfp": "Photo de profil de l'oganisateur du tournois",
|
||||
"upload.commonExplanation": "Avant que l'image ne soit visible publiquement, un modérateur la validera. Les images soumises par les Supporters n'ont pas besoin d'être validées.",
|
||||
"upload.afterExplanation_one": "Vous avez {{count}} image en attente. L'image sera visible une fois validée.",
|
||||
"upload.afterExplanation_many": "",
|
||||
"upload.afterExplanation_other": "Vous avez {{count}} images en attente. Les images seront visibles une fois validées.",
|
||||
"support.intro.first": "Salut! Je suis Sendou et sendou.ink est mon projet pour fournir des outils et ressources pour la communauté Splatoon. L'objectif est d'aider tout le monde à s'améliorer et à profiter de Splatoon, que vous soyez tout nouveau dans le jeu ou un vétéran chevronné.",
|
||||
"support.intro.second": "Si vous aimez ce que je fais, cette page détaille comment vous pouvez soutenir mon travail et gagner des avantages. Votre soutien m'aide à payer l'hébergement ainsi qu'à sponsoriser mon temps passé à améliorer continuellement le projet.",
|
||||
"support.action": "Soutenir sur Patreon",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "Evenements",
|
||||
"events.tabs.leaderboard": "Leaderboard",
|
||||
"edit.form.title": "Modification de l'organisation du tournoi",
|
||||
"edit.form.uploadLogo": "Télécharger le logo",
|
||||
"edit.form.socialLinks.title": "Liens de réseau saciaux",
|
||||
"edit.form.members.title": "Membres",
|
||||
"edit.form.members.info": "\"Admin\" Le rôle d'administrateur permet aux utilisateurs de modifier l'organisation et toute inscription à un tournoi hébergé par l'organisation. \"Organisateur\" and \"Streamer\" donnent le rôle correspondant dans les tournois. \"Membre\" n'a aucune autorisation spéciale.",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "הצטרפות",
|
||||
"actions.nevermind": "",
|
||||
"actions.upload": "העלאה",
|
||||
"actions.clickHere": "",
|
||||
"actions.goBack": "",
|
||||
"actions.enable": "",
|
||||
@@ -236,14 +235,6 @@
|
||||
"theme.auto": "אוטומטי",
|
||||
"websiteSubtitle": "מרכז Splatoon תחרותי",
|
||||
"upload.imageToUpload": "תמונה להעלאה",
|
||||
"upload.title": "מעלה את {{type}}. הגודל המומלץ הוא {{width}}×{{height}}.",
|
||||
"upload.type.team-pfp": "תמונת פרופיל של הקבוצה",
|
||||
"upload.type.team-banner": "תמונת באנר של הקבוצה",
|
||||
"upload.type.org-pfp": "",
|
||||
"upload.commonExplanation": "לפני שהתמונה תוצג לציבור, בודק יאמת אותה. תמונות שהועלו על ידי פטרונים מוצגות ללא אימות.",
|
||||
"upload.afterExplanation_one": "יש לכם תמונה {{count}} בהמתנה. התמונה תופיע אוטומטית לאחר האימות.",
|
||||
"upload.afterExplanation_two": "",
|
||||
"upload.afterExplanation_other": "יש לכם {{count}} תמונות בהמתנה. התמונות יופיעו אוטומטית לאחר האימות.",
|
||||
"support.intro.first": "שלום! אני Sendou ו- sendou.ink הוא הפרויקט שלי לספק כלים ומשאבים לקהילת Splatoon. המטרה היא לעזור לכולם להשתפר וליהנות מ-Splatoon בין אם אתם חדשים במשחק או שחקנים ותיקים.",
|
||||
"support.intro.second": "אם אתם אוהבים את מה שאני עושה העמוד הזה מפרט איך אתם יכול לתמוך בעבודה שלי ולהרוויח הטבות. התמיכה שלכם עוזרת לי לשלם עבור האירוח, וגם לתת חסות לזמן המושקע בשיפור המתמשך של הפרויקט.",
|
||||
"support.action": "תמיכה בפטראון",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "",
|
||||
"events.tabs.leaderboard": "",
|
||||
"edit.form.title": "",
|
||||
"edit.form.uploadLogo": "",
|
||||
"edit.form.socialLinks.title": "",
|
||||
"edit.form.members.title": "",
|
||||
"edit.form.members.info": "",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "Nessun contorno",
|
||||
"actions.join": "Entra",
|
||||
"actions.nevermind": "Non importa",
|
||||
"actions.upload": "Carica",
|
||||
"actions.clickHere": "Clicca qui",
|
||||
"actions.goBack": "Indietro",
|
||||
"actions.enable": "Attiva",
|
||||
@@ -237,14 +236,6 @@
|
||||
"theme.auto": "Automatico",
|
||||
"websiteSubtitle": "Hub per Splatoon Competitivo",
|
||||
"upload.imageToUpload": "Immagine da caricare",
|
||||
"upload.title": "Caricando {{type}}. La dimensione raccomandata è {{width}}×{{height}}.",
|
||||
"upload.type.team-pfp": "Foto profilo del team",
|
||||
"upload.type.team-banner": "Foto banner del team",
|
||||
"upload.type.org-pfp": "Foto profilo dell'organizzazione",
|
||||
"upload.commonExplanation": "Prima che l'immagine diventi visualizzabile pubblicamente, un moderatore dovrà approvarla. Le immagini caricate da iscritti al Patreon non passano attraverso questo processo di convalida.",
|
||||
"upload.afterExplanation_one": "Hai {{count}} immagine in coda. L'immagine verrà mostrata automaticamente dopo la convalida.",
|
||||
"upload.afterExplanation_many": "",
|
||||
"upload.afterExplanation_other": "Hai {{count}} immagini in coda. Le immagini verranno mostrate automaticamente dopo la convalida.",
|
||||
"support.intro.first": "Ciao! Sono Sendou e sendou.ink è il mio progetto per fornire tools e risorse alla community di Splatoon. L'obiettivo è quello di aiutare tutti a migliorare e divertirsi con Splatoon sia se sei un giocatore nuovo che un veterano.",
|
||||
"support.intro.second": "Se ti piace quel che sto facendo, questa pagina indica come tu possa supportare il mio lavoro e ottenere vantaggi. Il tuo supporto mi aiuta a coprire le spese di hosting così come supportare il tempo che ho speso sul migliorare continuamente il progetto.",
|
||||
"support.action": "Supporta su Patreon",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "Eventi",
|
||||
"events.tabs.leaderboard": "Classifica",
|
||||
"edit.form.title": "Modifica organizzazione torneo",
|
||||
"edit.form.uploadLogo": "Carica logo",
|
||||
"edit.form.socialLinks.title": "Link per social",
|
||||
"edit.form.members.title": "Membri",
|
||||
"edit.form.members.info": "Il ruolo \"Amministratore\" permette agli utenti di modificare l'organizzazione e ogni registrazione ai tornei gestiti dall'organizzazione. \"Organizzatore\" e \"Streamer\" possono dare il ruolo corrispondente nei tornei. \"Membro\" non ha nessun permesso speciale.",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "アウトラインなし",
|
||||
"actions.join": "参加する",
|
||||
"actions.nevermind": "やーめた",
|
||||
"actions.upload": "アップロード",
|
||||
"actions.clickHere": "ここをクリック",
|
||||
"actions.goBack": "戻る",
|
||||
"actions.enable": "",
|
||||
@@ -234,11 +233,6 @@
|
||||
"theme.auto": "自動",
|
||||
"websiteSubtitle": "Competitive Splatoon Hub",
|
||||
"upload.imageToUpload": "アップロードする画像",
|
||||
"upload.title": "{{type}}のアップロード: 推奨サイズは {{width}}×{{height}} です",
|
||||
"upload.type.team-pfp": "チームプロファイル画像",
|
||||
"upload.type.team-banner": "チームバナー",
|
||||
"upload.type.org-pfp": "トーナメント主催者プロファイル画像",
|
||||
"upload.commonExplanation": "画像が正式に使用される前に、モデレーターが承認を行います。パトロンによるアップロードの場合はチェックされません。",
|
||||
"support.intro.first": "やあ、ぼくは Sendou です。sendou.ink はぼくの個人プロジェクトで、スプラトゥーンコミュニティーのためのツールや情報などを開発しています。このプロジェクトの目的は、新規プレイヤーからベテランまですべての人がスプラトゥーンのウデマエを磨いたり楽しんだりできるようにすることです。",
|
||||
"support.intro.second": "もしこの活動を気に入ってくれたなら、このページでぼくの作業とモチベーションをサポートする方法を説明しています。あなたのサポートが、サーバーのホスティングやプロジェクトの改善のための開発に対する支援になります。",
|
||||
"support.action": "Patreon で支援する",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "",
|
||||
"events.tabs.leaderboard": "",
|
||||
"edit.form.title": "",
|
||||
"edit.form.uploadLogo": "",
|
||||
"edit.form.socialLinks.title": "",
|
||||
"edit.form.members.title": "",
|
||||
"edit.form.members.info": "",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "참여하기",
|
||||
"actions.nevermind": "",
|
||||
"actions.upload": "업로드",
|
||||
"actions.clickHere": "",
|
||||
"actions.goBack": "",
|
||||
"actions.enable": "",
|
||||
@@ -234,11 +233,6 @@
|
||||
"theme.auto": "자동",
|
||||
"websiteSubtitle": "Competitive Splatoon Hub",
|
||||
"upload.imageToUpload": "업로드할 이미지",
|
||||
"upload.title": "{{type}} 업로드 중. {{width}}×{{height}}의 크기를 추천합니다.",
|
||||
"upload.type.team-pfp": "팀 프로필 사진",
|
||||
"upload.type.team-banner": "팀 프로필 배너",
|
||||
"upload.type.org-pfp": "",
|
||||
"upload.commonExplanation": "이미지는 공개 전에 관리자의 확인을 거칩니다. 후원자들의 이미지는 확인 없이 공개됩니다.",
|
||||
"support.intro.first": "",
|
||||
"support.intro.second": "",
|
||||
"support.action": "",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "",
|
||||
"events.tabs.leaderboard": "",
|
||||
"edit.form.title": "",
|
||||
"edit.form.uploadLogo": "",
|
||||
"edit.form.socialLinks.title": "",
|
||||
"edit.form.members.title": "",
|
||||
"edit.form.members.info": "",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "",
|
||||
"actions.nevermind": "",
|
||||
"actions.upload": "",
|
||||
"actions.clickHere": "",
|
||||
"actions.goBack": "",
|
||||
"actions.enable": "",
|
||||
@@ -236,13 +235,6 @@
|
||||
"theme.auto": "",
|
||||
"websiteSubtitle": "Competitief Splatoon Hub",
|
||||
"upload.imageToUpload": "",
|
||||
"upload.title": "",
|
||||
"upload.type.team-pfp": "",
|
||||
"upload.type.team-banner": "",
|
||||
"upload.type.org-pfp": "",
|
||||
"upload.commonExplanation": "",
|
||||
"upload.afterExplanation_one": "",
|
||||
"upload.afterExplanation_other": "",
|
||||
"support.intro.first": "",
|
||||
"support.intro.second": "",
|
||||
"support.action": "",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "",
|
||||
"events.tabs.leaderboard": "",
|
||||
"edit.form.title": "",
|
||||
"edit.form.uploadLogo": "",
|
||||
"edit.form.socialLinks.title": "",
|
||||
"edit.form.members.title": "",
|
||||
"edit.form.members.info": "",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "Dołącz",
|
||||
"actions.nevermind": "",
|
||||
"actions.upload": "Wgraj",
|
||||
"actions.clickHere": "",
|
||||
"actions.goBack": "",
|
||||
"actions.enable": "",
|
||||
@@ -237,15 +236,6 @@
|
||||
"theme.auto": "Automatyczny",
|
||||
"websiteSubtitle": "Konkurencyjny ośrodek Splatoona",
|
||||
"upload.imageToUpload": "Zdjęcie do opublikowywania",
|
||||
"upload.title": "Opublikowywanie {{type}}. Zalecany rozmiar: {{width}}×{{height}}.",
|
||||
"upload.type.team-pfp": "Logo drużyny",
|
||||
"upload.type.team-banner": "Grafika drużyny",
|
||||
"upload.type.org-pfp": "",
|
||||
"upload.commonExplanation": "Zanim zdjęcie jest publicznie wyświetlone moderator musi je zweryfikować. Zdjęcia wstawione przez patronów nie przechodzą przez weryfikacje i są pokazywane automatycznie.",
|
||||
"upload.afterExplanation_one": "Masz {{count}} zdjęcie oczekujące weryfikacji. Zdjęcie pojawi się automatycznie po weryfikacji.",
|
||||
"upload.afterExplanation_few": "",
|
||||
"upload.afterExplanation_many": "",
|
||||
"upload.afterExplanation_other": "Masz {{count}} zdjęcia oczekujących weryfikacji. Zdjęcia pojawią się automatycznie po ich weryfikacji.",
|
||||
"support.intro.first": "Cześć! Jestem Sendou i sendou.ink jest moim projektem by dostarczyć narzędzia i zasoby dla społeczności Splatoon. Moim celem jest pomaganie każdemu się udoskonalić oraz bardziej cieszyć z Splatoona, czy jesteś początkowym graczem czy weteranem.",
|
||||
"support.intro.second": "Jeśli podoba ci się to, co robię, ta strona przedstawia jak możesz mnie wesprzeć oraz uzyskać korzyści. Twoje wsparcie pomaga mi opłacać hosting oraz sponsoruje mój czas spędzony na udoskonalanie projektu.",
|
||||
"support.action": "Wesprzyj na Patreonie",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "",
|
||||
"events.tabs.leaderboard": "",
|
||||
"edit.form.title": "",
|
||||
"edit.form.uploadLogo": "",
|
||||
"edit.form.socialLinks.title": "",
|
||||
"edit.form.members.title": "",
|
||||
"edit.form.members.info": "",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "Entrar",
|
||||
"actions.nevermind": "Deixa pra lá...",
|
||||
"actions.upload": "Fazer Upload",
|
||||
"actions.clickHere": "Clique aqui",
|
||||
"actions.goBack": "Voltar",
|
||||
"actions.enable": "",
|
||||
@@ -237,14 +236,6 @@
|
||||
"theme.auto": "Automático",
|
||||
"websiteSubtitle": "Centro do Splatoon Competitivo",
|
||||
"upload.imageToUpload": "Imagem para fazer o upload",
|
||||
"upload.title": "Fazendo o upload de {{type}}. O tamanho recomendado é {{width}}×{{height}}.",
|
||||
"upload.type.team-pfp": "imagem de perfil do time",
|
||||
"upload.type.team-banner": "imagem de capa do time",
|
||||
"upload.type.org-pfp": "",
|
||||
"upload.commonExplanation": "Antes da imagem ser mostrada publicamente, um moderador irá fazer a validação dela. Imagens enviadas por patronos ou patronesses são mostradas sem validação.",
|
||||
"upload.afterExplanation_one": "Você tem {{count}} imagem pendente. A imagem irá aparecer automaticamente após a validação.",
|
||||
"upload.afterExplanation_many": "",
|
||||
"upload.afterExplanation_other": "Você tem {{count}} imagens pendente. As imagens irão aparecer automaticamente após a validação.",
|
||||
"support.intro.first": "Oi! Eu sou o Sendou e o sendou.ink é o meu projeto que consiste em fornecer ferramentas e recursos para a comunidade do Splatoon. O objetivo é ajudar todos a aprimorar suas habilidades e aproveitar o Splatoon, não importando se você é um novato novinho em folha ou veterano de longa data.",
|
||||
"support.intro.second": "Se você gosta do que eu estou fazendo, essa página dá detalhes em como você pode apoiar meu trabalho e ganhar vantagens. Seu apoio me ajuda a pagar pela hospedagem do site e também patrocina o meu tempo investido em melhorar o projeto constantemente.",
|
||||
"support.action": "Apoiar no Patreon",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "",
|
||||
"events.tabs.leaderboard": "",
|
||||
"edit.form.title": "",
|
||||
"edit.form.uploadLogo": "",
|
||||
"edit.form.socialLinks.title": "",
|
||||
"edit.form.members.title": "",
|
||||
"edit.form.members.info": "",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "Без обводки",
|
||||
"actions.join": "Присоединиться",
|
||||
"actions.nevermind": "Отмена",
|
||||
"actions.upload": "Загрузить",
|
||||
"actions.clickHere": "Нажмите здесь",
|
||||
"actions.goBack": "Назад",
|
||||
"actions.enable": "Включить",
|
||||
@@ -237,15 +236,6 @@
|
||||
"theme.auto": "Системная",
|
||||
"websiteSubtitle": "Соревновательный Splatoon-хаб",
|
||||
"upload.imageToUpload": "Изображение для загрузки",
|
||||
"upload.title": "Загрузка {{type}}. Рекомендованный размер: {{width}}×{{height}}.",
|
||||
"upload.type.team-pfp": "Аватар комманды",
|
||||
"upload.type.team-banner": "Баннер команды",
|
||||
"upload.type.org-pfp": "Аватар организации",
|
||||
"upload.commonExplanation": "Перед отображением изображения для всех модератор проверит их. Изображения, загруженные подписчиками на Patreon, отображаются сразу, без проверки.",
|
||||
"upload.afterExplanation_one": "В ожидании {{count}} изображение. Оно автоматически появится после проверки.",
|
||||
"upload.afterExplanation_few": "",
|
||||
"upload.afterExplanation_many": "",
|
||||
"upload.afterExplanation_other": "В ожидании {{count}} изображения. Они автоматически появятся после проверки.",
|
||||
"support.intro.first": "Привет! Я Sendou и sendou.ink — мой проект, дающий инструменты и ресурсы для сообщества Splatoon. Цель состоит в том, чтобы помочь каждому стать лучше и наслаждаться Splatoon, независимо от того, являетесь ли вы новичком в игре или опытным ветераном.",
|
||||
"support.intro.second": "Если вам нравится то, что я делаю, на этой странице подробно расписано, как вы можете поддержать мой труд и получить привилегии. Ваша поддержка помогает мне оплачивать хостинг, а также спонсирует моё время, которое идёт на постоянное улучшение проекта.",
|
||||
"support.action": "Поддержать на Patreon",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "Турниры",
|
||||
"events.tabs.leaderboard": "Таблицы лидеров",
|
||||
"edit.form.title": "Редакция турнирной организации",
|
||||
"edit.form.uploadLogo": "Загрузить логотип",
|
||||
"edit.form.socialLinks.title": "Соц. ссылки",
|
||||
"edit.form.members.title": "Участники",
|
||||
"edit.form.members.info": "Роль \"Админ\" позволяет пользователю редактировать организацию и любую регистрацию на турниры, проводимые организацией. Роли \"Организатор\" и \"Стример\" даёт соответствующую роль в турнирах. Роль \"Участник\" не даёт особых привилегий.",
|
||||
|
||||
@@ -136,7 +136,6 @@
|
||||
"actions.noOutline": "",
|
||||
"actions.join": "加入",
|
||||
"actions.nevermind": "反悔",
|
||||
"actions.upload": "上传",
|
||||
"actions.clickHere": "点击这里",
|
||||
"actions.goBack": "返回",
|
||||
"actions.enable": "",
|
||||
@@ -234,11 +233,6 @@
|
||||
"theme.auto": "自动",
|
||||
"websiteSubtitle": "斯普拉遁竞技中心",
|
||||
"upload.imageToUpload": "上传图片",
|
||||
"upload.title": "上传 {{type}},推荐尺寸为 {{width}}×{{height}}。",
|
||||
"upload.type.team-pfp": "队伍头像",
|
||||
"upload.type.team-banner": "队伍横幅",
|
||||
"upload.type.org-pfp": "比赛组织头像",
|
||||
"upload.commonExplanation": "该图片需要经过管理员认证才能公开展示。Patron赞助者上传的图片不需要认证。",
|
||||
"support.intro.first": "您好!我是Sendou,sendou.ink是我的个人项目,用来给斯普拉遁社群提供工具和资源。我的目标是帮助新老玩家进步并享受这个游戏。",
|
||||
"support.intro.second": "如果您喜欢我所做的,本页详细展示了支持我的方式以及可获得的特权。您的支持将帮助我支付运营费用,并且赞助我持续完善这个项目。",
|
||||
"support.action": "成为Patreon支持者",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"events.tabs.events": "活动",
|
||||
"events.tabs.leaderboard": "排行榜",
|
||||
"edit.form.title": "编辑比赛组织",
|
||||
"edit.form.uploadLogo": "上传logo",
|
||||
"edit.form.socialLinks.title": "社交账号",
|
||||
"edit.form.members.title": "成员",
|
||||
"edit.form.members.info": "\"Admin\" 身份能让用户编辑组织信息以及该组织举办的比赛的报名信息。 \"Organizer\" 和 \"Streamer\" 能在比赛里被给予相对应的身份。 \"Member\" 没有特殊权限。",
|
||||
|
||||
Reference in New Issue
Block a user