Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbfda83e14 | ||
|
|
8fde1c9362 | ||
|
|
9febb8bb60 | ||
|
|
a754fc0282 | ||
|
|
6f17a5d4a8 | ||
|
|
9ed3f7169e | ||
|
|
c4a9d47180 | ||
|
|
8971748d5c | ||
|
|
0ac6636d08 | ||
|
|
f6faf97cdf | ||
|
|
6bb3ee4bc7 | ||
|
|
7bda138d69 | ||
|
|
9b51d61519 |
@@ -31,8 +31,8 @@
|
||||
"facehash": "^0.0.7",
|
||||
"framer-motion": "^12.23.12",
|
||||
"pocketbase": "^0.26.2",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"react-imask": "^7.6.1",
|
||||
"redaxios": "^0.5.1",
|
||||
"sonner": "^2.0.7",
|
||||
@@ -49,8 +49,8 @@
|
||||
"@lingui/cli": "^6.5.0",
|
||||
"@lingui/vite-plugin": "^6.5.0",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
@@ -655,9 +655,9 @@
|
||||
|
||||
"@types/node": ["@types/node@22.19.10", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||
"@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
"@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="],
|
||||
|
||||
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
||||
|
||||
@@ -1329,9 +1329,9 @@
|
||||
|
||||
"raf-schd": ["raf-schd@4.0.3", "", {}, "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="],
|
||||
|
||||
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
|
||||
"react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
|
||||
"react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="],
|
||||
|
||||
"react-imask": ["react-imask@7.6.1", "", { "dependencies": { "imask": "^7.6.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=0.14.0" } }, "sha512-vLNfzcCz62Yzx/GRGh5tiCph9Gbh2cZu+Tz8OiO5it2eNuuhpA0DWhhSlOtVtSJ80+Bx+vFK5De8eQ9AmbkXzA=="],
|
||||
|
||||
@@ -1397,7 +1397,7 @@
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
"scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
|
||||
|
||||
"scmp": ["scmp@2.1.0", "", {}, "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q=="],
|
||||
|
||||
|
||||
+4
-4
@@ -43,8 +43,8 @@
|
||||
"facehash": "^0.0.7",
|
||||
"framer-motion": "^12.23.12",
|
||||
"pocketbase": "^0.26.2",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"react-imask": "^7.6.1",
|
||||
"redaxios": "^0.5.1",
|
||||
"sonner": "^2.0.7",
|
||||
@@ -61,8 +61,8 @@
|
||||
"@lingui/cli": "^6.5.0",
|
||||
"@lingui/vite-plugin": "^6.5.0",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
|
||||
@@ -13,7 +13,6 @@ const { count, size, warnings } = await generateSW({
|
||||
'icon-192x192.png',
|
||||
'icon-512x512.png',
|
||||
'site.webmanifest',
|
||||
'styles.css',
|
||||
'push-handlers.js',
|
||||
],
|
||||
// Hand-written Web Push handlers (public/push-handlers.js) are pulled into
|
||||
|
||||
+17
-8
@@ -1,5 +1,5 @@
|
||||
// Grant or revoke the "Admin" role for a user by phone number (US +1 assumed).
|
||||
// Usage: bun run scripts/make-admin.ts <phone> [--revoke]
|
||||
// Grant or revoke a role for a user by phone number (US +1 assumed).
|
||||
// Usage: bun run scripts/make-admin.ts <phone> [--role Admin|Developer] [--revoke]
|
||||
import "dotenv/config";
|
||||
import SuperTokens from "supertokens-node";
|
||||
import Session from "supertokens-node/recipe/session";
|
||||
@@ -26,10 +26,19 @@ SuperTokens.init({
|
||||
],
|
||||
});
|
||||
|
||||
const VALID_ROLES = ["Admin", "Developer"] as const;
|
||||
|
||||
const raw = process.argv[2];
|
||||
const revoke = process.argv.includes("--revoke");
|
||||
const roleFlagIndex = process.argv.indexOf("--role");
|
||||
const role = roleFlagIndex === -1 ? "Admin" : process.argv[roleFlagIndex + 1];
|
||||
|
||||
if (!raw) {
|
||||
console.error("Usage: bun run scripts/make-admin.ts <phone> [--revoke]");
|
||||
console.error("Usage: bun run scripts/make-admin.ts <phone> [--role Admin|Developer] [--revoke]");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!VALID_ROLES.includes(role as (typeof VALID_ROLES)[number])) {
|
||||
console.error(`Invalid role "${role}". Valid roles: ${VALID_ROLES.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -60,15 +69,15 @@ if (!user) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await UserRoles.createNewRoleOrAddPermissions("Admin", []);
|
||||
await UserRoles.createNewRoleOrAddPermissions(role, []);
|
||||
|
||||
if (revoke) {
|
||||
const res = await UserRoles.removeUserRole("public", user.id, "Admin");
|
||||
console.log(`Removed Admin from ${matched} (user ${user.id}):`, res.status);
|
||||
const res = await UserRoles.removeUserRole("public", user.id, role);
|
||||
console.log(`Removed ${role} from ${matched} (user ${user.id}):`, res.status);
|
||||
} else {
|
||||
const res = await UserRoles.addRoleToUser("public", user.id, "Admin");
|
||||
const res = await UserRoles.addRoleToUser("public", user.id, role);
|
||||
console.log(
|
||||
`Granted Admin to ${matched} (user ${user.id}):`,
|
||||
`Granted ${role} to ${matched} (user ${user.id}):`,
|
||||
res.status === "OK"
|
||||
? res.didUserAlreadyHaveRole
|
||||
? "already had it"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
||||
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
import { DefaultCatchBoundary } from "../components/DefaultCatchBoundary";
|
||||
import RoutePending from "../components/route-pending";
|
||||
import { defaultHeaderConfig } from "@/features/core/hooks/use-router-config";
|
||||
|
||||
export function getRouter() {
|
||||
@@ -30,6 +31,9 @@ export function getRouter() {
|
||||
},
|
||||
defaultPreload: "intent",
|
||||
defaultPreloadStaleTime: 60_000,
|
||||
defaultPendingMs: 100,
|
||||
defaultPendingMinMs: 0,
|
||||
defaultPendingComponent: RoutePending,
|
||||
defaultErrorComponent: DefaultCatchBoundary,
|
||||
});
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import { getRootMeta } from "@/lib/i18n/meta";
|
||||
import FullScreenLoader from "@/components/full-screen-loader";
|
||||
import { CHROME_COLORS } from "@/lib/mantine/theme-colors";
|
||||
import mantineCssUrl from '@mantine/core/styles.css?url'
|
||||
import appStylesUrl from '../styles.css?url'
|
||||
import mantineDatesCssUrl from '@mantine/dates/styles.css?url'
|
||||
import mantineCarouselCssUrl from '@mantine/carousel/styles.css?url'
|
||||
import mantineTiptapCssUrl from '@mantine/tiptap/styles.css?url'
|
||||
@@ -101,6 +102,7 @@ export const Route = createRootRouteWithContext<{
|
||||
},
|
||||
{ rel: "manifest", href: "/site.webmanifest" },
|
||||
{ rel: "icon", href: "/favicon.ico" },
|
||||
{ rel: 'stylesheet', href: appStylesUrl },
|
||||
{ rel: 'stylesheet', href: mantineCssUrl },
|
||||
{ rel: 'stylesheet', href: mantineCarouselCssUrl },
|
||||
{ rel: 'stylesheet', href: mantineDatesCssUrl },
|
||||
@@ -183,7 +185,19 @@ function RootComponent() {
|
||||
ensureSuperTokensFrontend();
|
||||
initTelemetry(router);
|
||||
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js').catch(() => track('sw_register_failed'));
|
||||
// first install claims the page too; only reload on real updates
|
||||
const hadController = !!navigator.serviceWorker.controller;
|
||||
let reloading = false;
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
if (!hadController || reloading) return;
|
||||
reloading = true;
|
||||
window.location.reload();
|
||||
});
|
||||
navigator.serviceWorker.register('/sw.js').then((registration) => {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') registration.update().catch(() => {});
|
||||
});
|
||||
}).catch(() => track('sw_register_failed'));
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
@@ -231,7 +245,6 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
'try{var d=document.documentElement,t=localStorage.getItem("flxn-theme");if(t&&t!=="default"){d.setAttribute("data-theme",t);}var s=localStorage.getItem("flxn-theme-scheme");if(s){d.setAttribute("data-mantine-color-scheme",s);}var f=JSON.parse(localStorage.getItem("flxn-theme-fonts")||"[]");for(var i=0;i<f.length;i++){var l=document.createElement("link");l.rel="stylesheet";l.href=f[i];l.setAttribute("data-flxn-theme-font",t||"default");document.head.appendChild(l);}}catch(e){}',
|
||||
}}
|
||||
/>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
<body
|
||||
style={{
|
||||
@@ -243,6 +256,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
}}
|
||||
>
|
||||
<div className="app">{children}</div>
|
||||
<div className="flxn-theme-overlay" aria-hidden="true" />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Outlet, createFileRoute, useNavigate, useRouterState } from "@tanstack/react-router";
|
||||
import { Outlet, createFileRoute, redirect, useNavigate, useRouterState } from "@tanstack/react-router";
|
||||
import { Tabs } from "@mantine/core";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { msg } from "@lingui/core/macro";
|
||||
import { isDeveloperRole } from "@/features/core/utils/roles";
|
||||
|
||||
const TAB_ROUTES = {
|
||||
usage: "/admin/telemetry/usage",
|
||||
@@ -15,6 +16,11 @@ type TabValue = keyof typeof TAB_ROUTES;
|
||||
|
||||
export const Route = createFileRoute("/_authed/admin/telemetry")({
|
||||
component: TelemetryLayout,
|
||||
beforeLoad: ({ context }) => {
|
||||
if (!isDeveloperRole(context.auth?.roles)) {
|
||||
throw redirect({ to: "/admin" });
|
||||
}
|
||||
},
|
||||
loader: () => ({
|
||||
header: {
|
||||
title: msg`Telemetry`,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { tournamentQueries, useCurrentTournament } from "@/features/tournaments/queries";
|
||||
import UpcomingTournament from "@/features/tournaments/components/upcoming-tournament";
|
||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import { getServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||
import StartedTournament from "@/features/tournaments/components/started-tournament";
|
||||
import { Suspense } from "react";
|
||||
import UpcomingTournamentSkeleton from "@/features/tournaments/components/upcoming-tournament/skeleton";
|
||||
@@ -10,16 +11,13 @@ export const Route = createFileRoute("/_authed/")({
|
||||
component: () => <Suspense fallback={<UpcomingTournamentSkeleton />}>
|
||||
<Home />
|
||||
</Suspense>,
|
||||
beforeLoad: async ({ context }) => {
|
||||
const queryClient = context.queryClient;
|
||||
const tournament = await ensureServerQueryData(queryClient, tournamentQueries.current())
|
||||
|
||||
return { tournament }
|
||||
beforeLoad: ({ context }) => {
|
||||
prefetchServerQuery(context.queryClient, tournamentQueries.current());
|
||||
},
|
||||
loader: ({ context }) => ({
|
||||
withPadding: false,
|
||||
header: {
|
||||
title: context.tournament.name || "FLXN"
|
||||
title: getServerQueryData(context.queryClient, tournamentQueries.current())?.name || "FLXN"
|
||||
}
|
||||
}),
|
||||
pendingComponent: () => <UpcomingTournamentSkeleton />
|
||||
|
||||
@@ -14,26 +14,12 @@ const searchSchema = z.object({
|
||||
|
||||
export const Route = createFileRoute("/_authed/profile/$playerId")({
|
||||
validateSearch: searchSchema,
|
||||
beforeLoad: async ({ params, context }) => {
|
||||
beforeLoad: ({ params, context }) => {
|
||||
const { queryClient } = context;
|
||||
await Promise.all([
|
||||
prefetchServerQuery(
|
||||
queryClient,
|
||||
playerQueries.details(params.playerId)
|
||||
),
|
||||
prefetchServerQuery(
|
||||
queryClient,
|
||||
playerQueries.matches(params.playerId)
|
||||
),
|
||||
prefetchServerQuery(
|
||||
queryClient,
|
||||
playerQueries.stats(params.playerId)
|
||||
),
|
||||
prefetchServerQuery(
|
||||
queryClient,
|
||||
badgeQueries.playerBadges(params.playerId)
|
||||
),
|
||||
]);
|
||||
prefetchServerQuery(queryClient, playerQueries.details(params.playerId));
|
||||
prefetchServerQuery(queryClient, playerQueries.matches(params.playerId));
|
||||
prefetchServerQuery(queryClient, playerQueries.stats(params.playerId));
|
||||
prefetchServerQuery(queryClient, badgeQueries.playerBadges(params.playerId));
|
||||
},
|
||||
loader: ({ params, context }) => ({
|
||||
header: {
|
||||
|
||||
@@ -12,9 +12,9 @@ const searchSchema = z.object({
|
||||
|
||||
export const Route = createFileRoute("/_authed/teams/$teamId")({
|
||||
validateSearch: searchSchema,
|
||||
beforeLoad: async ({ params, context }) => {
|
||||
beforeLoad: ({ params, context }) => {
|
||||
const { queryClient } = context;
|
||||
await prefetchServerQuery(queryClient, teamQueries.details(params.teamId));
|
||||
prefetchServerQuery(queryClient, teamQueries.details(params.teamId));
|
||||
},
|
||||
loader: ({ params }) => ({
|
||||
header: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { msg } from "@lingui/core/macro";
|
||||
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
||||
import { isAdminRole } from "@/features/core/utils/roles";
|
||||
import { apiLoggingMiddleware } from "@/lib/telemetry/api-logging.server";
|
||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||
import { logger } from "@/lib/logger";
|
||||
@@ -19,7 +20,7 @@ export const Route = createFileRoute("/api/teams/upload-logo")({
|
||||
const i18n = localizedFor(context.metadata);
|
||||
try {
|
||||
const userId = context.userAuthId;
|
||||
const isAdmin = context.roles.includes("Admin");
|
||||
const isAdmin = isAdminRole(context.roles);
|
||||
|
||||
if (!userId) return new Response("Unauthenticated", { status: 401 });
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { msg } from "@lingui/core/macro";
|
||||
import { superTokensRequestMiddleware } from "@/utils/supertokens";
|
||||
import { isAdminRole } from "@/features/core/utils/roles";
|
||||
import { apiLoggingMiddleware } from "@/lib/telemetry/api-logging.server";
|
||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||
import { logger } from "@/lib/logger";
|
||||
@@ -19,7 +20,7 @@ export const Route = createFileRoute("/api/tournaments/upload-logo")({
|
||||
const i18n = localizedFor(context.metadata);
|
||||
try {
|
||||
const userId = context.userAuthId;
|
||||
const isAdmin = context.roles.includes("Admin");
|
||||
const isAdmin = isAdminRole(context.roles);
|
||||
|
||||
if (!userId) return new Response("Unauthenticated", { status: 401 });
|
||||
if (!isAdmin) return new Response("Unauthorized", { status: 403 });
|
||||
|
||||
@@ -50,6 +50,12 @@ textarea,
|
||||
transform: scale(0.90) translateY(-4px);
|
||||
}
|
||||
|
||||
/* themes paint their full-page texture through this element */
|
||||
.flxn-theme-overlay {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.app,
|
||||
[data-drawer-level] {
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Box } from "@mantine/core";
|
||||
import { FootballHelmetIcon } from "@phosphor-icons/react";
|
||||
|
||||
const HeadToHeadIcon = ({ size = 14 }: { size?: number }) => (
|
||||
<Box
|
||||
component="span"
|
||||
style={{
|
||||
position: "relative",
|
||||
display: "inline-block",
|
||||
width: size * (27.5 / 14),
|
||||
height: size * (16 / 14),
|
||||
}}
|
||||
>
|
||||
<FootballHelmetIcon
|
||||
size={size}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
transform: "rotate(25deg)",
|
||||
}}
|
||||
/>
|
||||
<FootballHelmetIcon
|
||||
size={size}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
transform: "scaleX(-1) rotate(25deg)",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default HeadToHeadIcon;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Skeleton, Stack } from "@mantine/core";
|
||||
|
||||
const RoutePending = () => (
|
||||
<Stack gap="md" p="md" w="100%">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={`route-pending-${index}`}
|
||||
height={96}
|
||||
w="100%"
|
||||
radius="md"
|
||||
style={{ opacity: Math.max(1 - index * 0.15, 0.3) }}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
export default RoutePending;
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ReactNode,
|
||||
useRef,
|
||||
useCallback,
|
||||
useMemo,
|
||||
startTransition,
|
||||
} from "react";
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
|
||||
@@ -48,6 +48,9 @@ function SwipeableTabs({
|
||||
}, [search?.tab, tabs, defaultTab]);
|
||||
|
||||
const [activeTab, setActiveTab] = useState(getActiveTabFromUrl);
|
||||
const [visitedTabs, setVisitedTabs] = useState<ReadonlySet<number>>(
|
||||
() => new Set([getActiveTabFromUrl()])
|
||||
);
|
||||
const [stuck, setStuck] = useState(false);
|
||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
|
||||
@@ -60,6 +63,9 @@ function SwipeableTabs({
|
||||
if (index === activeTab || index < 0 || index >= tabs.length) return;
|
||||
|
||||
setActiveTab(index);
|
||||
startTransition(() => {
|
||||
setVisitedTabs((prev) => (prev.has(index) ? prev : new Set(prev).add(index)));
|
||||
});
|
||||
embla?.scrollTo(index);
|
||||
onTabChange?.(index, tabs[index]);
|
||||
|
||||
@@ -225,7 +231,7 @@ function SwipeableTabs({
|
||||
{tabs.map((tab, index) => (
|
||||
<Carousel.Slide key={`${tab.label}-content-${index}`}>
|
||||
<Box ref={setSlideRef(index)} style={{ height: "auto", width: "100%", maxWidth: "100vw", overflow: "hidden" }}>
|
||||
{tab.content}
|
||||
{(visitedTabs.has(index) || index === activeTab) ? tab.content : null}
|
||||
</Box>
|
||||
</Carousel.Slide>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { superTokensAdminFunctionMiddleware } from "@/utils/supertokens";
|
||||
import { superTokensDeveloperFunctionMiddleware } from "@/utils/supertokens";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||
import { z } from "zod";
|
||||
@@ -17,7 +17,7 @@ const activitySearchParamsSchema = z.object({
|
||||
|
||||
export const searchActivities = createServerFn()
|
||||
.validator(activitySearchParamsSchema)
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.middleware([superTokensDeveloperFunctionMiddleware])
|
||||
.handler(async ({ data }) =>
|
||||
toServerResult<ActivityListResult>(async () => {
|
||||
const result = await pbAdmin.searchActivities(data);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { List, Text } from "@mantine/core";
|
||||
import { Plural, Trans, useLingui } from "@lingui/react/macro";
|
||||
import {
|
||||
DatabaseIcon,
|
||||
TreeStructureIcon,
|
||||
TrophyIcon,
|
||||
MedalIcon,
|
||||
@@ -10,12 +9,14 @@ import {
|
||||
} from "@phosphor-icons/react";
|
||||
import ListLink from "@/components/list-link";
|
||||
import ListButton from "@/components/list-button";
|
||||
import { useIsDeveloper } from "@/features/core/hooks/use-is-developer";
|
||||
import { getBadgeMigrationStatus, migrateBadgeProgress } from "@/features/badges/server";
|
||||
import { useServerQuery } from "@/lib/tanstack-query/hooks";
|
||||
import toast from "@/lib/sonner";
|
||||
|
||||
export const ToolsList = () => {
|
||||
const { t } = useLingui();
|
||||
const isDeveloper = useIsDeveloper();
|
||||
|
||||
const { data: migrationStatus, refetch: refetchMigrationStatus } = useServerQuery({
|
||||
queryKey: ["admin", "badge-migration-status"],
|
||||
@@ -51,11 +52,13 @@ export const ToolsList = () => {
|
||||
Icon={TrophyIcon}
|
||||
to="/admin/tournaments"
|
||||
/>
|
||||
{isDeveloper && (
|
||||
<ListLink
|
||||
label={t`Telemetry`}
|
||||
Icon={PulseIcon}
|
||||
to="/admin/telemetry/usage"
|
||||
/>
|
||||
)}
|
||||
<ListLink
|
||||
label={t`Award Badges`}
|
||||
Icon={CrownIcon}
|
||||
@@ -91,13 +94,6 @@ export const ToolsList = () => {
|
||||
<Trans>Badge recalculation failed. Check the server logs.</Trans>
|
||||
</Text>
|
||||
)}
|
||||
<ListButton
|
||||
label={t`Open Pocketbase`}
|
||||
Icon={DatabaseIcon}
|
||||
onClick={() =>
|
||||
window.location.replace(process.env.POCKETBASE_URL! + "/_/")
|
||||
}
|
||||
/>
|
||||
<ListLink
|
||||
label={t`Bracket Preview`}
|
||||
Icon={TreeStructureIcon}
|
||||
|
||||
@@ -49,6 +49,7 @@ export const BadgeIcon = ({ badge, filled, size = 48 }: BadgeIconProps & { size?
|
||||
width={size}
|
||||
height={size}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={() => setImageError(true)}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
||||
import { FootballHelmetIcon } from "@phosphor-icons/react";
|
||||
import HeadToHeadIcon from "@/components/head-to-head-icon";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { Match } from "@/features/matches/types";
|
||||
import TeamAvatar from "@/components/team-avatar";
|
||||
@@ -176,35 +176,19 @@ const MatchDock = ({ match, onClose }: MatchDockProps) => {
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
track("h2h_viewed", { source: "match_dock" });
|
||||
track("h2h_viewed", {
|
||||
source: "match_dock",
|
||||
context: [match?.home?.name, match?.away?.name]
|
||||
.filter(Boolean)
|
||||
.join(" v "),
|
||||
});
|
||||
h2hSheet.open();
|
||||
}}
|
||||
aria-label={t`View head-to-head`}
|
||||
w={40}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
<Group
|
||||
style={{ position: "relative", width: 27.5, height: 16 }}
|
||||
>
|
||||
<FootballHelmetIcon
|
||||
size={14}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
transform: "rotate(25deg)",
|
||||
}}
|
||||
/>
|
||||
<FootballHelmetIcon
|
||||
size={14}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
transform: "scaleX(-1) rotate(25deg)",
|
||||
}}
|
||||
/>
|
||||
</Group>
|
||||
<HeadToHeadIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { AppShell, Box } from '@mantine/core';
|
||||
import { AppShell } from '@mantine/core';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { useLocation } from '@tanstack/react-router';
|
||||
import Header from './header';
|
||||
import Navbar from './navbar';
|
||||
import Pullable from './pullable';
|
||||
import useVisualViewportSize from '../hooks/use-visual-viewport-size';
|
||||
import useRouterConfig from '../hooks/use-router-config';
|
||||
import Page from '@/components/page';
|
||||
import './route-transition.css';
|
||||
|
||||
const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const { header, withPadding, fullWidth } = useRouterConfig();
|
||||
const viewport = useVisualViewportSize();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
id='app-shell'
|
||||
layout='alt'
|
||||
// 0 = header collapse is instant; Mantine's default animates main's padding
|
||||
transitionDuration={0}
|
||||
header={{
|
||||
// Old installs froze black-translucent at add-to-home-screen time, so
|
||||
// the webview can extend under the status bar; inset is 0 elsewhere.
|
||||
@@ -48,16 +47,14 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
pos='relative'
|
||||
h='100%'
|
||||
mah='100%'
|
||||
pb={{ base: 'calc(65px + env(safe-area-inset-bottom, 0px))', sm: 0 }}
|
||||
pb={{ base: 'calc(73px + env(safe-area-inset-bottom, 0px))', sm: 0 }}
|
||||
px={{ base: 0.01, sm: 100, md: 200, lg: 300 }}
|
||||
maw='100dvw'
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
<Pullable>
|
||||
<Page noPadding={!withPadding} fullWidth={fullWidth}>
|
||||
<Box key={location.pathname} className="flxn-route-enter">
|
||||
{children}
|
||||
</Box>
|
||||
</Page>
|
||||
</Pullable>
|
||||
</AppShell.Main>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
import { useRouterState } from "@tanstack/react-router";
|
||||
|
||||
const TRAVEL_MS = 333;
|
||||
|
||||
export const useNavIndicator = (isMobile: boolean) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const indicatorRef = useRef<HTMLSpanElement>(null);
|
||||
const shapeRef = useRef<HTMLSpanElement>(null);
|
||||
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const indicator = indicatorRef.current;
|
||||
const shape = shapeRef.current;
|
||||
if (!container || !indicator || !shape) return;
|
||||
|
||||
let last: number | null = null;
|
||||
|
||||
const place = () => {
|
||||
const active = container.querySelector<HTMLElement>("[data-active]");
|
||||
if (!active) {
|
||||
delete indicator.dataset.placed;
|
||||
last = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const x = active.offsetLeft;
|
||||
const y = active.offsetTop;
|
||||
const travelled = isMobile ? x : y;
|
||||
const first = indicator.dataset.placed !== "1";
|
||||
|
||||
if (first) indicator.style.transition = "none";
|
||||
indicator.style.setProperty("--nav-x", `${x}px`);
|
||||
indicator.style.setProperty("--nav-y", `${y}px`);
|
||||
indicator.style.setProperty("--nav-w", `${active.offsetWidth}px`);
|
||||
indicator.style.setProperty("--nav-h", `${active.offsetHeight}px`);
|
||||
indicator.dataset.placed = "1";
|
||||
|
||||
if (first) {
|
||||
indicator.getBoundingClientRect();
|
||||
indicator.style.transition = "";
|
||||
} else if (
|
||||
last !== null &&
|
||||
Math.abs(travelled - last) > 1 &&
|
||||
!window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
) {
|
||||
const axis = isMobile ? "scaleX" : "scaleY";
|
||||
shape.animate(
|
||||
[
|
||||
{ transform: `${axis}(1)`, offset: 0 },
|
||||
{ transform: `${axis}(0.88)`, offset: 0.4 },
|
||||
{ transform: `${axis}(1)`, offset: 1 },
|
||||
],
|
||||
{ duration: TRAVEL_MS, easing: "ease-in-out" }
|
||||
);
|
||||
}
|
||||
last = travelled;
|
||||
};
|
||||
|
||||
place();
|
||||
const observer = new ResizeObserver(place);
|
||||
observer.observe(container);
|
||||
return () => observer.disconnect();
|
||||
}, [pathname, isMobile]);
|
||||
|
||||
return { containerRef, indicatorRef, shapeRef };
|
||||
};
|
||||
@@ -32,30 +32,19 @@ export const NavLink = ({
|
||||
<Box
|
||||
component={Link}
|
||||
to={href}
|
||||
className={styles.navLinkBox}
|
||||
className={`flxn-nav-link ${styles.navLinkBox}`}
|
||||
p={{ base: 0, sm: 8 }}
|
||||
aria-label={label}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-active={isActive || undefined}
|
||||
>
|
||||
<Flex
|
||||
direction={{ base: "column", md: "row" }}
|
||||
align="center"
|
||||
gap={{ base: 0, md: "xs" }}
|
||||
>
|
||||
<Icon
|
||||
weight={isActive ? "fill" : "regular"}
|
||||
size={28}
|
||||
style={{
|
||||
color: isActive ? "var(--mantine-primary-color-filled)" : undefined,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
visibleFrom="md"
|
||||
ta="center"
|
||||
size="md"
|
||||
fw={isActive ? 800 : 500}
|
||||
c={isActive ? "var(--mantine-primary-color-filled)" : undefined}
|
||||
>
|
||||
<Icon weight={isActive ? "fill" : "regular"} size={28} />
|
||||
<Text visibleFrom="md" ta="center" size="md" fw={isActive ? 800 : 500}>
|
||||
{label}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -11,11 +11,6 @@
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.navLinkBox svg,
|
||||
.navLinkBox p {
|
||||
transition: color 150ms ease-out;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.navLinkBox {
|
||||
transition: transform 150ms cubic-bezier(0.32, 0.72, 0, 1);
|
||||
|
||||
@@ -5,11 +5,13 @@ import { useAuth } from "@/contexts/auth-context";
|
||||
import { useLinks } from "../hooks/use-links";
|
||||
import { memo } from "react";
|
||||
import { useIsPWA } from "@/hooks/use-is-pwa";
|
||||
import { useNavIndicator } from "./nav-indicator/use-nav-indicator";
|
||||
|
||||
const Navbar = () => {
|
||||
const { user, roles } = useAuth()
|
||||
const isMobile = useIsMobile();
|
||||
const isPWA = useIsPWA();
|
||||
const { containerRef, indicatorRef, shapeRef } = useNavIndicator(isMobile);
|
||||
|
||||
const links = useLinks(user?.id, roles);
|
||||
|
||||
@@ -17,9 +19,16 @@ const Navbar = () => {
|
||||
? 'max(env(safe-area-inset-bottom, 0px), 0.5rem)'
|
||||
: 'env(safe-area-inset-bottom, 0px)';
|
||||
|
||||
const indicator = (
|
||||
<span ref={indicatorRef} className="flxn-nav-indicator" aria-hidden="true">
|
||||
<span ref={shapeRef} className="flxn-nav-indicator-shape" />
|
||||
</span>
|
||||
);
|
||||
|
||||
if (isMobile) return (
|
||||
<Paper component='nav' role='navigation' withBorder shadow="sm" radius='lg' h='4rem' w='calc(100% - 1rem)' pos='fixed' m='0.5rem' bottom={bottomOffset} style={{ zIndex: 10 }}>
|
||||
<Group gap='xs' justify='space-around' h='100%' w='100%' px={{ base: 12, sm: 0 }}>
|
||||
<Paper className="flxn-nav" component='nav' role='navigation' withBorder shadow="sm" radius='lg' h='4rem' w='calc(100% - 1rem)' pos='fixed' m='0.5rem' bottom={bottomOffset} style={{ zIndex: 10 }}>
|
||||
<Group ref={containerRef} pos='relative' gap='xs' justify='space-around' h='100%' w='100%' px={{ base: 12, sm: 0 }}>
|
||||
{indicator}
|
||||
{links.map((link) => (
|
||||
<NavLink key={link.href} {...link} />
|
||||
))}
|
||||
@@ -27,9 +36,10 @@ const Navbar = () => {
|
||||
</Paper>
|
||||
)
|
||||
|
||||
return <AppShell.Navbar p="xs" role='navigation'>
|
||||
return <AppShell.Navbar className="flxn-nav" p="xs" role='navigation'>
|
||||
<AppShell.Section grow component={ScrollArea}>
|
||||
<Stack gap='xs' mx='auto' w='fit-content' justify='end' mt='md'>
|
||||
<Stack ref={containerRef} pos='relative' gap='xs' mx='auto' w='fit-content' justify='end' mt='md'>
|
||||
{indicator}
|
||||
{links.map((link) => (
|
||||
<NavLink key={link.href} {...link} />
|
||||
))}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.flxn-route-enter {
|
||||
animation: flxn-route-fade-in 180ms ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes flxn-route-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { isDeveloperRole } from "../utils/roles";
|
||||
|
||||
export const useIsDeveloper = (): boolean => {
|
||||
const { roles } = useAuth();
|
||||
return isDeveloperRole(roles);
|
||||
};
|
||||
@@ -1,4 +1,8 @@
|
||||
export const ADMIN_ROLE = "Admin";
|
||||
export const DEVELOPER_ROLE = "Developer";
|
||||
|
||||
export const isDeveloperRole = (roles?: string[] | null): boolean =>
|
||||
!!roles?.includes(DEVELOPER_ROLE);
|
||||
|
||||
export const isAdminRole = (roles?: string[] | null): boolean =>
|
||||
!!roles?.includes(ADMIN_ROLE);
|
||||
!!roles?.includes(ADMIN_ROLE) || isDeveloperRole(roles);
|
||||
|
||||
@@ -21,7 +21,7 @@ const useConsumeCode = (onWrongCode: () => void) => {
|
||||
} else {
|
||||
queryClient.setQueryData(playerKeys.auth, response.data);
|
||||
toast.success(t`Successfully logged in. Welcome back!`);
|
||||
navigate({ to: '/' })
|
||||
window.location.href = '/';
|
||||
}
|
||||
} else if (data.status === 'INCORRECT_USER_INPUT_CODE_ERROR') {
|
||||
onWrongCode();
|
||||
|
||||
@@ -21,7 +21,7 @@ const useCreateUser = () => {
|
||||
...old,
|
||||
user: data
|
||||
}));
|
||||
navigate({ to: '/' });
|
||||
window.location.href = '/';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Text, Group, Stack, Paper, Indicator, Box, Tooltip, ActionIcon } from "@mantine/core";
|
||||
import { FootballHelmetIcon } from "@phosphor-icons/react";
|
||||
import HeadToHeadIcon from "@/components/head-to-head-icon";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { Match } from "../types";
|
||||
import TeamAvatar from "@/components/team-avatar";
|
||||
@@ -42,7 +42,10 @@ const MatchCard = ({ match, hideH2H = false }: MatchCardProps) => {
|
||||
|
||||
const handleH2HClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
track("h2h_viewed", { source: "match_card" });
|
||||
track("h2h_viewed", {
|
||||
source: "match_card",
|
||||
context: [match.home?.name, match.away?.name].filter(Boolean).join(" v "),
|
||||
});
|
||||
h2hSheet.open();
|
||||
};
|
||||
|
||||
@@ -91,26 +94,7 @@ const MatchCard = ({ match, hideH2H = false }: MatchCardProps) => {
|
||||
aria-label={t`View head-to-head`}
|
||||
w={40}
|
||||
>
|
||||
<Group style={{ position: 'relative', width: 27.5, height: 16 }}>
|
||||
<FootballHelmetIcon
|
||||
size={14}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
transform: 'rotate(25deg)',
|
||||
}}
|
||||
/>
|
||||
<FootballHelmetIcon
|
||||
size={14}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
transform: 'scaleX(-1) rotate(25deg)',
|
||||
}}
|
||||
/>
|
||||
</Group>
|
||||
<HeadToHeadIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Stack, Text } from "@mantine/core";
|
||||
import { MantineSpacing, Stack, Text } from "@mantine/core";
|
||||
import { useMemo } from "react";
|
||||
import { Match } from "../types";
|
||||
import MatchCard from "./match-card";
|
||||
@@ -7,9 +7,10 @@ import { Trans } from "@lingui/react/macro";
|
||||
interface MatchListProps {
|
||||
matches: Match[];
|
||||
hideH2H?: boolean;
|
||||
p?: MantineSpacing;
|
||||
}
|
||||
|
||||
const MatchList = ({ matches, hideH2H = false }: MatchListProps) => {
|
||||
const MatchList = ({ matches, hideH2H = false, p = "md" }: MatchListProps) => {
|
||||
const filteredMatches = useMemo(
|
||||
() =>
|
||||
[...(matches ?? [])]
|
||||
@@ -27,7 +28,7 @@ const MatchList = ({ matches, hideH2H = false }: MatchListProps) => {
|
||||
const isRegional = filteredMatches[0]?.tournament?.regional;
|
||||
|
||||
return (
|
||||
<Stack p="md" gap="sm">
|
||||
<Stack p={p} gap="sm">
|
||||
{isRegional && (
|
||||
<Text size="xs" c="dimmed" ta="center" px="md">
|
||||
<Trans>Matches for regionals are unordered</Trans>
|
||||
|
||||
@@ -200,10 +200,10 @@ const TeamHeadToHeadContent = ({ team1, team2, isOpen = true }: TeamHeadToHeadSh
|
||||
</Stack>
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={600} px="md">
|
||||
<Text size="sm" fw={600}>
|
||||
<Trans>Match History (<Plural value={totalMatches} one="# match" other="# matches" />)</Trans>
|
||||
</Text>
|
||||
<MatchList matches={matches} hideH2H />
|
||||
<MatchList matches={matches} hideH2H p={0} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -53,7 +53,12 @@ const LeagueHeadToHead = () => {
|
||||
setPlayer1Id(playerId);
|
||||
} else {
|
||||
setPlayer2Id(playerId);
|
||||
track("h2h_viewed", { source: "stats" });
|
||||
const opponent =
|
||||
allPlayerStats?.find((p) => p.player_id === playerId)?.player_name || "";
|
||||
track("h2h_viewed", {
|
||||
source: "stats",
|
||||
context: [player1Name, opponent].filter(Boolean).join(" v "),
|
||||
});
|
||||
h2hSheet.open();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -260,10 +260,10 @@ const PlayerHeadToHeadContent = ({
|
||||
</Stack>
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={600} px="md">
|
||||
<Text size="sm" fw={600}>
|
||||
<Trans>Match History ({totalMatches})</Trans>
|
||||
</Text>
|
||||
<MatchList matches={matches} hideH2H />
|
||||
<MatchList matches={matches} hideH2H p={0} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -293,11 +293,10 @@ const PlayerStatsTable = ({ viewType = 'all' }: PlayerStatsTableProps) => {
|
||||
}, [navigate]);
|
||||
|
||||
const handleSort = (key: SortKey) => {
|
||||
track("stats_sorted", { value: key });
|
||||
setSortConfig((prev) => ({
|
||||
key,
|
||||
direction: prev.key === key && prev.direction === "desc" ? "asc" : "desc",
|
||||
}));
|
||||
const direction =
|
||||
sortConfig.key === key && sortConfig.direction === "desc" ? "asc" : "desc";
|
||||
track("stats_sorted", { value: key, context: direction });
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
const getSortIcon = (key: SortKey) => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Sheet from "@/components/sheet/sheet";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { Flex, Title, ActionIcon, Stack, Button, Box } from "@mantine/core";
|
||||
import { PencilIcon, FootballHelmetIcon } from "@phosphor-icons/react";
|
||||
import { Flex, Title, ActionIcon, Stack, Button } from "@mantine/core";
|
||||
import { PencilIcon } from "@phosphor-icons/react";
|
||||
import HeadToHeadIcon from "@/components/head-to-head-icon";
|
||||
import { useMemo } from "react";
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import NameUpdateForm from "./name-form";
|
||||
@@ -55,32 +56,16 @@ const Header = ({ player }: HeaderProps) => {
|
||||
size="sm"
|
||||
radius="xl"
|
||||
onClick={() => {
|
||||
track("h2h_viewed", { source: "profile" });
|
||||
track("h2h_viewed", {
|
||||
source: "profile",
|
||||
context: [player.first_name, player.last_name].filter(Boolean).join(" "),
|
||||
});
|
||||
h2hSheet.open();
|
||||
}}
|
||||
w={40}
|
||||
display={!owner ? 'block' : 'none'}
|
||||
>
|
||||
<Box style={{ position: 'relative', width: 27.5, height: 16 }}>
|
||||
<FootballHelmetIcon
|
||||
size={14}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
transform: 'rotate(25deg)'
|
||||
}}
|
||||
/>
|
||||
<FootballHelmetIcon
|
||||
size={14}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
transform: 'scaleX(-1) rotate(25deg)'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<HeadToHeadIcon />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { CaretLeftIcon, CaretRightIcon, InfoIcon } from "@phosphor-icons/react";
|
||||
import { CaretLeftIcon, CaretRightIcon } from "@phosphor-icons/react";
|
||||
import HeadToHeadIcon from "@/components/head-to-head-icon";
|
||||
import WizardOrbIcon from "@/components/wizard-orb-icon";
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
@@ -218,14 +219,17 @@ export const PredictionEditor: React.FC<PredictionEditorProps> = ({
|
||||
radius="md"
|
||||
onClick={() => {
|
||||
if (activeResolved?.home && activeResolved?.away) {
|
||||
track("h2h_viewed", { source: "predictions" });
|
||||
track("h2h_viewed", {
|
||||
source: "predictions",
|
||||
context: `${activeResolved.home.name} v ${activeResolved.away.name}`,
|
||||
});
|
||||
}
|
||||
matchupSheet.open();
|
||||
}}
|
||||
disabled={!activeResolved?.home && !activeResolved?.away}
|
||||
aria-label={t`Matchup details`}
|
||||
>
|
||||
<InfoIcon size={16} />
|
||||
<HeadToHeadIcon />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<Group gap="sm" align="center" wrap="nowrap">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Box, Title, Stack, Switch, Button, Text, Group } from "@mantine/core";
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { useWebPush } from "@/hooks/use-web-push";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { useIsAdmin } from "@/features/core/hooks/use-is-admin";
|
||||
import { useIsDeveloper } from "@/features/core/hooks/use-is-developer";
|
||||
import { updateUserTelemetryAlerts } from "@/features/settings/server";
|
||||
import toast from "@/lib/sonner";
|
||||
|
||||
@@ -40,7 +40,7 @@ const TelemetryAlertsSwitch = () => {
|
||||
export function NotificationsSection() {
|
||||
const { supported, configured, permission, subscribed, busy, enable, disable, sendTest } =
|
||||
useWebPush();
|
||||
const isAdmin = useIsAdmin();
|
||||
const isDeveloper = useIsDeveloper();
|
||||
const { t } = useLingui();
|
||||
|
||||
if (!supported || !configured) {
|
||||
@@ -99,7 +99,7 @@ export function NotificationsSection() {
|
||||
disabled={busy}
|
||||
label={t`Enable notifications on this device`}
|
||||
/>
|
||||
{subscribed && isAdmin && <TelemetryAlertsSwitch />}
|
||||
{subscribed && isDeveloper && <TelemetryAlertsSwitch />}
|
||||
{subscribed && (
|
||||
<Group>
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import {
|
||||
superTokensAdminFunctionMiddleware,
|
||||
superTokensDeveloperFunctionMiddleware,
|
||||
superTokensFunctionMiddleware,
|
||||
} from "@/utils/supertokens";
|
||||
|
||||
@@ -145,7 +145,7 @@ export const updateUserAccentColor = createServerFn({ method: "POST" })
|
||||
|
||||
export const updateUserTelemetryAlerts = createServerFn({ method: "POST" })
|
||||
.validator((data: boolean) => data === true)
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.middleware([superTokensDeveloperFunctionMiddleware])
|
||||
.handler(async ({ context, data }) => {
|
||||
const { userAuthId, metadata } = context;
|
||||
if (!userAuthId) return;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { superTokensFunctionMiddleware, superTokensAdminFunctionMiddleware } from "@/utils/supertokens";
|
||||
import { isAdminRole } from "@/features/core/utils/roles";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||
import { z } from "zod";
|
||||
@@ -35,7 +36,7 @@ export const createTeam = createServerFn()
|
||||
.handler(async ({ data, context }) =>
|
||||
toServerResult(async () => {
|
||||
const userId = context.userAuthId;
|
||||
const isAdmin = context.roles.includes("Admin");
|
||||
const isAdmin = isAdminRole(context.roles);
|
||||
|
||||
//if (!isAdmin && !data.players.includes(userId)) {
|
||||
// throw new Error("You can only create teams that include yourself as a player");
|
||||
@@ -57,7 +58,7 @@ export const updateTeam = createServerFn()
|
||||
.handler(async ({ data: { id, updates }, context }) =>
|
||||
toServerResult(async () => {
|
||||
const userId = context.userAuthId;
|
||||
const isAdmin = context.roles.includes("Admin");
|
||||
const isAdmin = isAdminRole(context.roles);
|
||||
|
||||
const team = await pbAdmin.getTeam(id);
|
||||
if (!team) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { z } from "zod";
|
||||
import { superTokensAdminFunctionMiddleware } from "@/utils/supertokens";
|
||||
import { superTokensDeveloperFunctionMiddleware } from "@/utils/supertokens";
|
||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
||||
import {
|
||||
@@ -37,7 +37,7 @@ const clientEventSearchSchema = z.object({
|
||||
|
||||
export const searchClientEvents = createServerFn()
|
||||
.validator(clientEventSearchSchema)
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.middleware([superTokensDeveloperFunctionMiddleware])
|
||||
.handler(async ({ data }) =>
|
||||
toServerResult<TelemetryListResult<ClientEvent>>(async () => {
|
||||
const result = await pbAdmin.searchClientEvents(data);
|
||||
@@ -58,7 +58,7 @@ const clientErrorSearchSchema = z.object({
|
||||
|
||||
export const searchClientErrors = createServerFn()
|
||||
.validator(clientErrorSearchSchema)
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.middleware([superTokensDeveloperFunctionMiddleware])
|
||||
.handler(async ({ data }) =>
|
||||
toServerResult<TelemetryListResult<ClientError>>(async () => {
|
||||
const result = await pbAdmin.searchClientErrors(data);
|
||||
@@ -73,59 +73,75 @@ const errorGroupsSchema = z.object({
|
||||
limit: z.number().max(100).optional(),
|
||||
});
|
||||
|
||||
// Aggregates raw rows, not rollups: a brand-new error must appear immediately,
|
||||
// not on the next rollup tick.
|
||||
export const getClientErrorGroups = createServerFn()
|
||||
.validator(errorGroupsSchema)
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.middleware([superTokensDeveloperFunctionMiddleware])
|
||||
.handler(async ({ data }) =>
|
||||
toServerResult<ClientErrorGroup[]>(async () => {
|
||||
const { from, to, includeResolved = false, limit = 50 } = data;
|
||||
|
||||
const rollups = await pbAdmin.getRollupRange({
|
||||
metrics: ["client_error.count"],
|
||||
from,
|
||||
to,
|
||||
});
|
||||
const toExclusive = new Date(`${to}T00:00:00.000Z`);
|
||||
toExclusive.setUTCDate(toExclusive.getUTCDate() + 1);
|
||||
const filter = `created >= '${from} 00:00:00.000Z' && created < '${toExclusive
|
||||
.toISOString()
|
||||
.replace("T", " ")}'`;
|
||||
|
||||
const countByGroup = new Map<string, number>();
|
||||
for (const rollup of rollups) {
|
||||
if (!rollup.dim) continue;
|
||||
countByGroup.set(rollup.dim, (countByGroup.get(rollup.dim) ?? 0) + rollup.value);
|
||||
}
|
||||
|
||||
const topGroups = [...countByGroup.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, limit);
|
||||
|
||||
const groups: ClientErrorGroup[] = [];
|
||||
for (const [groupHash, count] of topGroups) {
|
||||
const latest = await pbAdmin.searchClientErrors({ groupHash, perPage: 1 });
|
||||
const sample = latest.items[0];
|
||||
if (!sample) continue;
|
||||
|
||||
const resolved = !!sample.resolved;
|
||||
if (resolved && !includeResolved) continue;
|
||||
|
||||
groups.push({
|
||||
group_hash: groupHash,
|
||||
count,
|
||||
lastSeen: sample.created,
|
||||
resolved,
|
||||
const byGroup = new Map<string, ClientErrorGroup>();
|
||||
await pbAdmin.pageCollection<{
|
||||
group_hash: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
source?: string;
|
||||
path?: string;
|
||||
resolved?: boolean;
|
||||
created: string;
|
||||
}>(
|
||||
"client_errors",
|
||||
{ filter, fields: "group_hash,message,stack,source,path,resolved,created" },
|
||||
(rows) => {
|
||||
for (const row of rows) {
|
||||
const existing = byGroup.get(row.group_hash);
|
||||
if (existing) {
|
||||
existing.count++;
|
||||
// Paged in created-asc order, so each row is the newest so far.
|
||||
existing.lastSeen = row.created;
|
||||
existing.resolved = !!row.resolved;
|
||||
existing.sample = {
|
||||
message: row.message,
|
||||
stack: row.stack,
|
||||
path: row.path,
|
||||
source: row.source,
|
||||
};
|
||||
} else {
|
||||
byGroup.set(row.group_hash, {
|
||||
group_hash: row.group_hash,
|
||||
count: 1,
|
||||
lastSeen: row.created,
|
||||
resolved: !!row.resolved,
|
||||
sample: {
|
||||
message: sample.message,
|
||||
stack: sample.stack,
|
||||
path: sample.path,
|
||||
source: sample.source,
|
||||
message: row.message,
|
||||
stack: row.stack,
|
||||
path: row.path,
|
||||
source: row.source,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return groups;
|
||||
return [...byGroup.values()]
|
||||
.filter((group) => includeResolved || !group.resolved)
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, limit);
|
||||
})
|
||||
);
|
||||
|
||||
export const resolveClientErrorGroup = createServerFn({ method: "POST" })
|
||||
.validator(z.object({ groupHash: z.string(), resolved: z.boolean().optional() }))
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.middleware([superTokensDeveloperFunctionMiddleware])
|
||||
.handler(async ({ data }) =>
|
||||
toServerResult<{ updated: number }>(async () => {
|
||||
const updated = await pbAdmin.resolveErrorsByGroup(data.groupHash, data.resolved ?? true);
|
||||
@@ -142,7 +158,7 @@ const rollupRangeSchema = z.object({
|
||||
|
||||
export const getRollupRange = createServerFn()
|
||||
.validator(rollupRangeSchema)
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.middleware([superTokensDeveloperFunctionMiddleware])
|
||||
.handler(async ({ data }) =>
|
||||
toServerResult<RollupRow[]>(async () => pbAdmin.getRollupRange(data))
|
||||
);
|
||||
@@ -151,7 +167,7 @@ export const listTelemetryAlerts = createServerFn()
|
||||
.validator(
|
||||
z.object({ page: z.number().optional(), perPage: z.number().max(200).optional() })
|
||||
)
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.middleware([superTokensDeveloperFunctionMiddleware])
|
||||
.handler(async ({ data }) =>
|
||||
toServerResult<TelemetryListResult<TelemetryAlert>>(async () =>
|
||||
pbAdmin.listTelemetryAlerts(data.page, data.perPage)
|
||||
@@ -181,7 +197,7 @@ export const listSessions = createServerFn()
|
||||
perPage: z.number().max(100).optional(),
|
||||
})
|
||||
)
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.middleware([superTokensDeveloperFunctionMiddleware])
|
||||
.handler(async ({ data }) =>
|
||||
toServerResult<TelemetryListResult<SessionSummary>>(async () => {
|
||||
const { days = 7, page = 1, perPage = 30 } = data;
|
||||
@@ -269,7 +285,7 @@ const MAX_TIMELINE_ENTRIES = 500;
|
||||
|
||||
export const getSessionTimeline = createServerFn()
|
||||
.validator(z.object({ sessionId: z.string().min(8).max(64) }))
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.middleware([superTokensDeveloperFunctionMiddleware])
|
||||
.handler(async ({ data }) =>
|
||||
toServerResult<SessionTimeline>(async () => {
|
||||
const events = await pbAdmin.searchClientEvents({
|
||||
@@ -303,16 +319,17 @@ export const getSessionTimeline = createServerFn()
|
||||
for (const event of events.items) {
|
||||
if (event.name.startsWith("vital.")) continue;
|
||||
const props = event.props as
|
||||
| { emoji?: string; source?: string; tournament?: string; value?: string }
|
||||
| { emoji?: string; source?: string; tournament?: string; value?: string; context?: string }
|
||||
| null
|
||||
| undefined;
|
||||
const breakdown =
|
||||
props?.emoji ?? props?.value ?? props?.source ?? props?.tournament;
|
||||
const detail = [breakdown, props?.context].filter(Boolean).join(" · ");
|
||||
entries.push({
|
||||
ts: event.created,
|
||||
kind: event.name === "page_view" ? "page_view" : "event",
|
||||
label: event.name === "page_view" ? event.path || event.route_id || "?" : event.name,
|
||||
detail: event.name === "page_view" ? undefined : (breakdown ?? event.path),
|
||||
detail: event.name === "page_view" ? undefined : (detail || event.path),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -409,7 +426,7 @@ export const getSessionTimeline = createServerFn()
|
||||
);
|
||||
|
||||
export const getTelemetryRuntimeStatus = createServerFn()
|
||||
.middleware([superTokensAdminFunctionMiddleware])
|
||||
.middleware([superTokensDeveloperFunctionMiddleware])
|
||||
.handler(async () =>
|
||||
toServerResult<TelemetryRuntimeStatus>(async () => {
|
||||
ensureTelemetryScheduler();
|
||||
|
||||
@@ -2,11 +2,15 @@ import { Group, Stack, ThemeIcon, Text } from "@mantine/core";
|
||||
import { Tournament } from "../../types";
|
||||
import { CalendarIcon, MapPinIcon, TrophyIcon } from "@phosphor-icons/react";
|
||||
import { useMemo } from "react";
|
||||
import { useMounted } from "@mantine/hooks";
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import GlitchAvatar from "@/components/glitch-avatar";
|
||||
|
||||
const Header = ({ tournament }: { tournament: Tournament }) => {
|
||||
const { i18n } = useLingui();
|
||||
// Local-timezone text cannot be server-rendered: the pod formats in UTC and
|
||||
// hydration mismatches on every load.
|
||||
const mounted = useMounted();
|
||||
const tournamentStart = useMemo(
|
||||
() => new Date(tournament.start_time),
|
||||
[tournament.start_time]
|
||||
@@ -48,6 +52,7 @@ const Header = ({ tournament }: { tournament: Tournament }) => {
|
||||
<CalendarIcon size={14} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" c="dimmed" lineClamp={1}>
|
||||
{mounted && (
|
||||
<Trans>
|
||||
{tournamentStart.toLocaleDateString(i18n.locale, {
|
||||
weekday: "short",
|
||||
@@ -60,6 +65,7 @@ const Header = ({ tournament }: { tournament: Tournament }) => {
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TournamentInfo } from "../types";
|
||||
import { Fragment, useCallback } from "react";
|
||||
import React from "react";
|
||||
import { TrophyIcon, CalendarIcon, MapPinIcon } from "@phosphor-icons/react";
|
||||
import { useMounted } from "@mantine/hooks";
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
|
||||
interface TournamentListProps {
|
||||
@@ -18,6 +19,7 @@ interface TournamentListItemProps {
|
||||
|
||||
const TournamentListItem = React.memo(({ tournament }: TournamentListItemProps) => {
|
||||
const { i18n } = useLingui();
|
||||
const mounted = useMounted();
|
||||
const startDate = tournament.start_time ? new Date(tournament.start_time) : null;
|
||||
|
||||
return (
|
||||
@@ -43,7 +45,8 @@ const TournamentListItem = React.memo(({ tournament }: TournamentListItemProps)
|
||||
<CalendarIcon size={10} />
|
||||
</ThemeIcon>
|
||||
<Text size="xs" c="dimmed">
|
||||
{startDate.toLocaleDateString(i18n.locale, {
|
||||
{mounted &&
|
||||
startDate.toLocaleDateString(i18n.locale, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { superTokensAdminFunctionMiddleware, superTokensFunctionMiddleware } from "@/utils/supertokens";
|
||||
import { isAdminRole } from "@/features/core/utils/roles";
|
||||
import { createServerFn } from "@tanstack/react-start";
|
||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||
import { tournamentInputSchema, GroupStanding } from "@/features/tournaments/types";
|
||||
@@ -46,7 +47,7 @@ export const getTournament = createServerFn()
|
||||
.validator(z.string())
|
||||
.middleware([superTokensFunctionMiddleware])
|
||||
.handler(async ({ data: tournamentId, context }) => {
|
||||
const isAdmin = context.roles.includes("Admin");
|
||||
const isAdmin = isAdminRole(context.roles);
|
||||
return toServerResult(() => pbAdmin.getTournament(tournamentId, isAdmin));
|
||||
});
|
||||
|
||||
@@ -64,7 +65,7 @@ export const enrollTeam = createServerFn()
|
||||
.handler(async ({ data: { tournamentId, teamId }, context }) =>
|
||||
toServerResult(async () => {
|
||||
const userId = context.userAuthId;
|
||||
const isAdmin = context.roles.includes("Admin");
|
||||
const isAdmin = isAdminRole(context.roles);
|
||||
|
||||
const team = await pbAdmin.getTeam(teamId);
|
||||
if (!team) { throw new Error('Team not found'); }
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { QueryClient, QueryKey } from "@tanstack/react-query";
|
||||
import { ServerResult } from "../types";
|
||||
|
||||
export function getServerQueryData<TData>(
|
||||
queryClient: QueryClient,
|
||||
query: {
|
||||
queryKey: QueryKey;
|
||||
queryFn: () => Promise<ServerResult<TData>>;
|
||||
}
|
||||
): TData | undefined {
|
||||
return queryClient.getQueryData<TData>(query.queryKey);
|
||||
}
|
||||
|
||||
export async function ensureServerQueryData<TData>(
|
||||
queryClient: QueryClient,
|
||||
query: {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { msg } from "@lingui/core/macro";
|
||||
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||
import { sendPushToPlayer } from "@/lib/push";
|
||||
import { Logger } from "@/lib/logger";
|
||||
import { ADMIN_ROLE } from "@/features/core/utils/roles";
|
||||
import { DEVELOPER_ROLE } from "@/features/core/utils/roles";
|
||||
|
||||
const logger = new Logger("Telemetry > Alerts");
|
||||
|
||||
@@ -22,23 +22,23 @@ const fnFailureThreshold = (): number => {
|
||||
|
||||
const pbTimestamp = (date: Date): string => date.toISOString().replace("T", " ");
|
||||
|
||||
const ADMIN_CACHE_TTL_MS = 10 * 60 * 1000;
|
||||
let adminPlayersCache: { playerIds: string[]; expiresAt: number } | null = null;
|
||||
const DEVELOPER_CACHE_TTL_MS = 10 * 60 * 1000;
|
||||
let developerPlayersCache: { playerIds: string[]; expiresAt: number } | null = null;
|
||||
|
||||
const getAdminPlayerIds = async (): Promise<string[]> => {
|
||||
const getDeveloperPlayerIds = async (): Promise<string[]> => {
|
||||
const now = Date.now();
|
||||
if (adminPlayersCache && adminPlayersCache.expiresAt > now) {
|
||||
return adminPlayersCache.playerIds;
|
||||
if (developerPlayersCache && developerPlayersCache.expiresAt > now) {
|
||||
return developerPlayersCache.playerIds;
|
||||
}
|
||||
|
||||
const UserRoles = (await import("supertokens-node/recipe/userroles")).default;
|
||||
const UserMetadata = (await import("supertokens-node/recipe/usermetadata")).default;
|
||||
const response = await UserRoles.getUsersThatHaveRole("public", ADMIN_ROLE);
|
||||
const response = await UserRoles.getUsersThatHaveRole("public", DEVELOPER_ROLE);
|
||||
const users = response.status === "OK" ? response.users : [];
|
||||
const playerIds: string[] = [];
|
||||
for (const userId of users) {
|
||||
try {
|
||||
// Opt-in: only admins who enabled telemetry alerts in settings.
|
||||
// Opt-in: only developers who enabled telemetry alerts in settings.
|
||||
const { metadata } = await UserMetadata.getUserMetadata(userId);
|
||||
if (metadata?.telemetryAlerts !== true) continue;
|
||||
const player = await pbAdmin.getPlayerByAuthId(userId);
|
||||
@@ -46,7 +46,7 @@ const getAdminPlayerIds = async (): Promise<string[]> => {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
adminPlayersCache = { playerIds, expiresAt: now + ADMIN_CACHE_TTL_MS };
|
||||
developerPlayersCache = { playerIds, expiresAt: now + DEVELOPER_CACHE_TTL_MS };
|
||||
return playerIds;
|
||||
};
|
||||
|
||||
@@ -71,8 +71,8 @@ const fireAlert = async (candidate: AlertCandidate) => {
|
||||
value: candidate.value,
|
||||
});
|
||||
|
||||
const admins = await getAdminPlayerIds();
|
||||
for (const playerId of admins) {
|
||||
const developers = await getDeveloperPlayerIds();
|
||||
for (const playerId of developers) {
|
||||
try {
|
||||
await sendPushToPlayer(playerId, {
|
||||
title: candidate.title,
|
||||
|
||||
+101
-105
@@ -13,7 +13,7 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/features/matches/components/match-card.tsx:80
|
||||
#: src/features/matches/components/match-card.tsx:83
|
||||
msgid " (Losers)"
|
||||
msgstr " (Verlierer)"
|
||||
|
||||
@@ -93,7 +93,7 @@ msgstr "{0, plural, one {Spieler angemeldet} other {Spieler angemeldet}}"
|
||||
|
||||
#. placeholder {0}: tournamentStart.toLocaleDateString(i18n.locale, { weekday: "short", month: "short", day: "numeric", })
|
||||
#. placeholder {1}: tournamentStart.toLocaleTimeString(i18n.locale, { hour: "2-digit", minute: "2-digit", })
|
||||
#: src/features/tournaments/components/started-tournament/header.tsx:51
|
||||
#: src/features/tournaments/components/started-tournament/header.tsx:56
|
||||
msgid "{0} at {1}"
|
||||
msgstr "{0} um {1}"
|
||||
|
||||
@@ -228,7 +228,7 @@ msgstr "{namesList} und 1 weitere"
|
||||
msgid "{size}-team knockout"
|
||||
msgstr "{size}-Team-K.-o.-Runde"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:446
|
||||
#: src/features/players/components/player-stats-table.tsx:445
|
||||
msgid "* Confidence penalty applied for players with <15 matches"
|
||||
msgstr "* Vertrauensabschlag für Spieler mit <15 Spielen"
|
||||
|
||||
@@ -241,7 +241,7 @@ msgstr "* Falls nötig"
|
||||
msgid "* PICKS shows correct picks / total picks made"
|
||||
msgstr "* PICKS zeigt richtige Picks / insgesamt abgegebene Picks"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:449
|
||||
#: src/features/players/components/player-stats-table.tsx:448
|
||||
msgid "** Not an official rating"
|
||||
msgstr "** Keine offizielle Wertung"
|
||||
|
||||
@@ -254,19 +254,19 @@ msgstr "&"
|
||||
msgid "#{0}"
|
||||
msgstr "#{0}"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:416
|
||||
#: src/features/players/components/player-stats-table.tsx:415
|
||||
msgid "• <0>AC:</0> Average Cups Per Match"
|
||||
msgstr "• <0>AC:</0> Durchschnittliche Becher pro Spiel"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:419
|
||||
#: src/features/players/components/player-stats-table.tsx:418
|
||||
msgid "• <0>ACA:</0> Average Cups Against"
|
||||
msgstr "• <0>ACA:</0> Durchschnittlich kassierte Becher"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:413
|
||||
#: src/features/players/components/player-stats-table.tsx:412
|
||||
msgid "• <0>ALM:</0> Average Loss Margin"
|
||||
msgstr "• <0>ALM:</0> Durchschnittlicher Rückstand bei Niederlage"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:410
|
||||
#: src/features/players/components/player-stats-table.tsx:409
|
||||
msgid "• <0>AWM:</0> Average Win Margin"
|
||||
msgstr "• <0>AWM:</0> Durchschnittlicher Vorsprung bei Sieg"
|
||||
|
||||
@@ -274,15 +274,15 @@ msgstr "• <0>AWM:</0> Durchschnittlicher Vorsprung bei Sieg"
|
||||
msgid "• <0>Bracket reset:</0> only picked if your bracket triggers it — worth double the Final"
|
||||
msgstr "• <0>Bracket-Reset:</0> wird nur gewertet, wenn dein Bracket ihn auslöst — zählt doppelt so viel wie das Finale"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:425
|
||||
#: src/features/players/components/player-stats-table.tsx:424
|
||||
msgid "• <0>CA:</0> Cups Against"
|
||||
msgstr "• <0>CA:</0> Kassierte Becher"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:422
|
||||
#: src/features/players/components/player-stats-table.tsx:421
|
||||
msgid "• <0>CF:</0> Cups For"
|
||||
msgstr "• <0>CF:</0> Erzielte Becher"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:404
|
||||
#: src/features/players/components/player-stats-table.tsx:403
|
||||
msgid "• <0>L:</0> Losses"
|
||||
msgstr "• <0>L:</0> Niederlagen"
|
||||
|
||||
@@ -290,23 +290,23 @@ msgstr "• <0>L:</0> Niederlagen"
|
||||
msgid "• <0>Losers bracket:</0> 5, 10, 20, 40…"
|
||||
msgstr "• <0>Verlierer-Bracket:</0> 5, 10, 20, 40…"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:392
|
||||
#: src/features/players/components/player-stats-table.tsx:391
|
||||
msgid "• <0>M:</0> Matches"
|
||||
msgstr "• <0>M:</0> Spiele"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:398
|
||||
#: src/features/players/components/player-stats-table.tsx:397
|
||||
msgid "• <0>MMR:</0> Matchmaking Rating"
|
||||
msgstr "• <0>MMR:</0> Matchmaking-Wertung"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:395
|
||||
#: src/features/players/components/player-stats-table.tsx:394
|
||||
msgid "• <0>T:</0> Tournaments"
|
||||
msgstr "• <0>T:</0> Turniere"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:401
|
||||
#: src/features/players/components/player-stats-table.tsx:400
|
||||
msgid "• <0>W:</0> Wins"
|
||||
msgstr "• <0>W:</0> Siege"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:407
|
||||
#: src/features/players/components/player-stats-table.tsx:406
|
||||
msgid "• <0>W%:</0> Win Percentage"
|
||||
msgstr "• <0>W%:</0> Siegquote"
|
||||
|
||||
@@ -314,11 +314,11 @@ msgstr "• <0>W%:</0> Siegquote"
|
||||
msgid "• <0>Winners bracket:</0> 10, 20, 40, 80…"
|
||||
msgstr "• <0>Gewinner-Bracket:</0> 10, 20, 40, 80…"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:437
|
||||
#: src/features/players/components/player-stats-table.tsx:436
|
||||
msgid "• Average Cups/Match (25%)"
|
||||
msgstr "• Durchschnittliche Becher/Spiel (25%)"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:440
|
||||
#: src/features/players/components/player-stats-table.tsx:439
|
||||
msgid "• Average Win Margin (15%)"
|
||||
msgstr "• Durchschnittlicher Vorsprung bei Sieg (15%)"
|
||||
|
||||
@@ -326,11 +326,11 @@ msgstr "• Durchschnittlicher Vorsprung bei Sieg (15%)"
|
||||
msgid "• Each correct pick earns points, doubling every round"
|
||||
msgstr "• Jeder richtige Tipp bringt Punkte, die sich jede Runde verdoppeln"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:443
|
||||
#: src/features/players/components/player-stats-table.tsx:442
|
||||
msgid "• Match Volume Bonus (10%)"
|
||||
msgstr "• Match-Volumen-Bonus (10%)"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:434
|
||||
#: src/features/players/components/player-stats-table.tsx:433
|
||||
msgid "• Win Rate (50%)"
|
||||
msgstr "• Siegquote (50%)"
|
||||
|
||||
@@ -457,9 +457,9 @@ msgstr "Admin"
|
||||
msgid "Aero"
|
||||
msgstr "Aero"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:65
|
||||
#: src/app/routes/_authed/stats.tsx:68
|
||||
#: src/features/activities/components/activities-table.tsx:92
|
||||
#: src/features/players/components/profile/index.tsx:35
|
||||
#: src/features/players/components/profile/index.tsx:37
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
@@ -572,7 +572,7 @@ msgstr "Ø Sieg-Marge"
|
||||
msgid "Award Badge"
|
||||
msgstr "Abzeichen verleihen"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:60
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:63
|
||||
msgid "Award Badges"
|
||||
msgstr "Abzeichen verleihen"
|
||||
|
||||
@@ -606,15 +606,15 @@ msgstr "Zurück"
|
||||
msgid "Badge awarded to {playerName}"
|
||||
msgstr "Abzeichen an {playerName} verliehen"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:91
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:94
|
||||
msgid "Badge recalculation failed. Check the server logs."
|
||||
msgstr "Abzeichen-Neuberechnung fehlgeschlagen. Prüfe die Server-Logs."
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:39
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:40
|
||||
msgid "Badge recalculation is already running"
|
||||
msgstr "Abzeichen-Neuberechnung läuft bereits"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:38
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:39
|
||||
msgid "Badge recalculation started in the background"
|
||||
msgstr "Abzeichen-Neuberechnung im Hintergrund gestartet"
|
||||
|
||||
@@ -622,7 +622,7 @@ msgstr "Abzeichen-Neuberechnung im Hintergrund gestartet"
|
||||
msgid "Badge Type"
|
||||
msgstr "Abzeichentyp"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:66
|
||||
#: src/features/players/components/profile/index.tsx:68
|
||||
msgid "Badges"
|
||||
msgstr "Abzeichen"
|
||||
|
||||
@@ -645,7 +645,7 @@ msgid "Bracket"
|
||||
msgstr "Turnierbaum"
|
||||
|
||||
#: src/app/routes/_authed/admin/preview.tsx:13
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:102
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:98
|
||||
msgid "Bracket Preview"
|
||||
msgstr "Turnierbaum-Vorschau"
|
||||
|
||||
@@ -720,7 +720,7 @@ msgstr "Wähle einen Spieler"
|
||||
msgid "Claymation"
|
||||
msgstr "Knete"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:102
|
||||
#: src/features/players/components/league-head-to-head.tsx:107
|
||||
msgid "Clear selected player"
|
||||
msgstr "Ausgewählten Spieler entfernen"
|
||||
|
||||
@@ -1118,7 +1118,7 @@ msgstr "Fehlerspitzen und fehlschlagende Funktionen, als Push an dieses Konto"
|
||||
msgid "Error: {errorMessage}"
|
||||
msgstr "Fehler: {errorMessage}"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:57
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:63
|
||||
#: src/features/telemetry/components/usage-screen.tsx:260
|
||||
msgid "Errors"
|
||||
msgstr "Fehler"
|
||||
@@ -1186,7 +1186,7 @@ msgstr "Aktives Gerät konnte nicht festgelegt werden"
|
||||
msgid "Failed to set volume"
|
||||
msgstr "Lautstärke konnte nicht festgelegt werden"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:42
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:43
|
||||
msgid "Failed to start badge recalculation"
|
||||
msgstr "Abzeichen-Neuberechnung konnte nicht gestartet werden"
|
||||
|
||||
@@ -1198,8 +1198,8 @@ msgstr "Team konnte nicht aktualisiert werden"
|
||||
msgid "Failed to update tournament"
|
||||
msgstr "Turnier konnte nicht aktualisiert werden"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:141
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:139
|
||||
#: src/app/routes/api/teams/upload-logo.ts:142
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:140
|
||||
#: src/features/teams/components/team-form/index.tsx:148
|
||||
#: src/features/tournaments/components/tournament-form.tsx:88
|
||||
msgid "Failed to upload logo"
|
||||
@@ -1209,8 +1209,8 @@ msgstr "Logo konnte nicht hochgeladen werden"
|
||||
msgid "Failure"
|
||||
msgstr "Fehler"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:80
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:81
|
||||
#: src/app/routes/api/teams/upload-logo.ts:81
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:82
|
||||
msgid "File too large. Maximum size is 10MB."
|
||||
msgstr "Datei zu groß. Maximale Größe ist 10 MB."
|
||||
|
||||
@@ -1264,7 +1264,7 @@ msgstr "Funktion"
|
||||
msgid "Function Name"
|
||||
msgstr "Funktionsname"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:51
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:57
|
||||
msgid "Functions"
|
||||
msgstr "Funktionen"
|
||||
|
||||
@@ -1333,13 +1333,13 @@ msgstr "viel Spaß, viel Glück!"
|
||||
msgid "Have you played before?"
|
||||
msgstr "Hast du schon mal mitgespielt?"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:53
|
||||
#: src/app/routes/_authed/stats.tsx:56
|
||||
#: src/features/bracket/components/match-dock.tsx:174
|
||||
#: src/features/bracket/components/match-dock.tsx:219
|
||||
#: src/features/matches/components/match-card.tsx:86
|
||||
#: src/features/matches/components/match-card.tsx:230
|
||||
#: src/features/players/components/league-head-to-head.tsx:193
|
||||
#: src/features/players/components/profile/header.tsx:94
|
||||
#: src/features/bracket/components/match-dock.tsx:203
|
||||
#: src/features/matches/components/match-card.tsx:89
|
||||
#: src/features/matches/components/match-card.tsx:214
|
||||
#: src/features/players/components/league-head-to-head.tsx:198
|
||||
#: src/features/players/components/profile/header.tsx:79
|
||||
msgid "Head to Head"
|
||||
msgstr "Direktvergleich"
|
||||
|
||||
@@ -1401,8 +1401,8 @@ msgstr "Falscher Code"
|
||||
msgid "Invalid action"
|
||||
msgstr "Ungültige Aktion"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:66
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:67
|
||||
#: src/app/routes/api/teams/upload-logo.ts:67
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:68
|
||||
msgid "Invalid file type. Only JPEG, PNG and GIF are allowed."
|
||||
msgstr "Ungültiger Dateityp. Nur JPEG, PNG und GIF sind erlaubt."
|
||||
|
||||
@@ -1410,8 +1410,8 @@ msgstr "Ungültiger Dateityp. Nur JPEG, PNG und GIF sind erlaubt."
|
||||
msgid "Invalid hex color format"
|
||||
msgstr "Ungültiges Hex-Farbformat"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:34
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:35
|
||||
#: src/app/routes/api/teams/upload-logo.ts:35
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:36
|
||||
msgid "Invalid input"
|
||||
msgstr "Ungültige Eingabe"
|
||||
|
||||
@@ -1470,7 +1470,7 @@ msgid "Last name must be 3-20 characters long and contain only letters"
|
||||
msgstr "Der Nachname muss 3-20 Zeichen lang sein und darf nur Buchstaben enthalten"
|
||||
|
||||
#. placeholder {0}: migrationStatus.result.totalBadgesEarned
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:79
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:82
|
||||
msgid "Last recalculation: {0, plural, one {# badge earned} other {# badges earned}}"
|
||||
msgstr "Letzte Neuberechnung: {0, plural, one {# Abzeichen verdient} other {# Abzeichen verdient}}"
|
||||
|
||||
@@ -1504,8 +1504,8 @@ msgstr "Ort ist erforderlich"
|
||||
msgid "Logo"
|
||||
msgstr "Logo"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:47
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:48
|
||||
#: src/app/routes/api/teams/upload-logo.ts:48
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:49
|
||||
msgid "Logo file is required"
|
||||
msgstr "Logo-Datei ist erforderlich"
|
||||
|
||||
@@ -1530,8 +1530,8 @@ msgstr "Niederlagen"
|
||||
msgid "M"
|
||||
msgstr "S"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:72
|
||||
#: src/features/players/components/profile/index.tsx:36
|
||||
#: src/app/routes/_authed/stats.tsx:75
|
||||
#: src/features/players/components/profile/index.tsx:38
|
||||
msgid "Mainline"
|
||||
msgstr "Mainline"
|
||||
|
||||
@@ -1559,7 +1559,7 @@ msgid "Manage Team Songs/Logos"
|
||||
msgstr "Team-Songs/Logos verwalten"
|
||||
|
||||
#: src/app/routes/_authed/admin/tournaments/index.tsx:17
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:50
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:51
|
||||
msgid "Manage Tournaments"
|
||||
msgstr "Turniere verwalten"
|
||||
|
||||
@@ -1611,8 +1611,8 @@ msgstr "Match-Verlauf ({totalMatches})"
|
||||
msgid "Match started!"
|
||||
msgstr "Match gestartet!"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:376
|
||||
#: src/features/players/components/profile/index.tsx:85
|
||||
#: src/features/players/components/player-stats-table.tsx:375
|
||||
#: src/features/players/components/profile/index.tsx:87
|
||||
#: src/features/players/components/profile/skeleton.tsx:70
|
||||
#: src/features/teams/components/team-profile/index.tsx:44
|
||||
#: src/features/teams/components/team-profile/skeleton.tsx:75
|
||||
@@ -1621,7 +1621,7 @@ msgstr "Match gestartet!"
|
||||
msgid "Matches"
|
||||
msgstr "Matches"
|
||||
|
||||
#: src/features/matches/components/match-list.tsx:33
|
||||
#: src/features/matches/components/match-list.tsx:34
|
||||
msgid "Matches for regionals are unordered"
|
||||
msgstr "Matches für Regionals sind ungeordnet"
|
||||
|
||||
@@ -1630,11 +1630,11 @@ msgstr "Matches für Regionals sind ungeordnet"
|
||||
msgid "Matches Played"
|
||||
msgstr "Gespielte Matches"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:249
|
||||
#: src/features/predictions/components/prediction-editor.tsx:253
|
||||
msgid "Matchup"
|
||||
msgstr "Matchup"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:226
|
||||
#: src/features/predictions/components/prediction-editor.tsx:230
|
||||
msgid "Matchup details"
|
||||
msgstr "Matchup-Details"
|
||||
|
||||
@@ -1642,20 +1642,20 @@ msgstr "Matchup-Details"
|
||||
msgid "MAU"
|
||||
msgstr "MAU"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:123
|
||||
#: src/features/players/components/league-head-to-head.tsx:128
|
||||
msgid "Me"
|
||||
msgstr "Ich"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:65
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:68
|
||||
msgid "Migrate Badge Progress"
|
||||
msgstr "Abzeichen-Fortschritt migrieren"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:131
|
||||
#: src/features/players/components/player-stats-table.tsx:356
|
||||
#: src/features/players/components/player-stats-table.tsx:355
|
||||
msgid "MMR"
|
||||
msgstr "MMR"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:431
|
||||
#: src/features/players/components/player-stats-table.tsx:430
|
||||
msgid "MMR Calculation:"
|
||||
msgstr "MMR-Berechnung:"
|
||||
|
||||
@@ -1712,7 +1712,7 @@ msgstr "Neuer Client-Fehler"
|
||||
msgid "New Player"
|
||||
msgstr "Neuer Spieler"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:211
|
||||
#: src/features/predictions/components/prediction-editor.tsx:212
|
||||
msgid "Next match"
|
||||
msgstr "Nächstes Match"
|
||||
|
||||
@@ -1775,7 +1775,7 @@ msgstr "Keine Matches geplant"
|
||||
msgid "No player activity found"
|
||||
msgstr "Keine Spieleraktivität gefunden"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:151
|
||||
#: src/features/players/components/league-head-to-head.tsx:156
|
||||
msgid "No players available"
|
||||
msgstr "Keine Spieler verfügbar"
|
||||
|
||||
@@ -1784,8 +1784,8 @@ msgstr "Keine Spieler verfügbar"
|
||||
msgid "No players enrolled yet"
|
||||
msgstr "Noch keine Spieler angemeldet"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:151
|
||||
#: src/features/players/components/player-stats-table.tsx:486
|
||||
#: src/features/players/components/league-head-to-head.tsx:156
|
||||
#: src/features/players/components/player-stats-table.tsx:485
|
||||
msgid "No players found matching \"{search}\""
|
||||
msgstr "Keine Spieler gefunden, die zu \"{search}\" passen"
|
||||
|
||||
@@ -1818,7 +1818,7 @@ msgstr "Kein Song ausgewählt"
|
||||
msgid "No state captured"
|
||||
msgstr "Kein Status erfasst"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:319
|
||||
#: src/features/players/components/player-stats-table.tsx:318
|
||||
msgid "No Stats Available"
|
||||
msgstr "Keine Statistiken verfügbar"
|
||||
|
||||
@@ -1843,7 +1843,7 @@ msgstr "Noch keine Teams angemeldet"
|
||||
msgid "No teams found matching \"{search}\""
|
||||
msgstr "Keine Teams gefunden, die zu \"{search}\" passen"
|
||||
|
||||
#: src/features/tournaments/components/tournament-list.tsx:95
|
||||
#: src/features/tournaments/components/tournament-list.tsx:98
|
||||
msgid "No tournaments found"
|
||||
msgstr "Keine Turniere gefunden"
|
||||
|
||||
@@ -1908,10 +1908,6 @@ msgstr "Sobald du angemeldet bist, kannst du andere Free Agents und ihre Telefon
|
||||
msgid "One time code"
|
||||
msgstr "Einmalcode"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:95
|
||||
msgid "Open Pocketbase"
|
||||
msgstr "Pocketbase öffnen"
|
||||
|
||||
#: src/features/bracket/components/match-report.tsx:273
|
||||
msgid "Opponent"
|
||||
msgstr "Gegner"
|
||||
@@ -1937,7 +1933,7 @@ msgstr "Die Tipps anderer Spieler sind versteckt, bis das Turnier beginnt."
|
||||
msgid "Overtime count is required"
|
||||
msgstr "Anzahl der Verlängerungen ist erforderlich"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:61
|
||||
#: src/features/players/components/profile/index.tsx:63
|
||||
#: src/features/players/components/profile/skeleton.tsx:65
|
||||
#: src/features/teams/components/team-profile/index.tsx:25
|
||||
#: src/features/teams/components/team-profile/skeleton.tsx:70
|
||||
@@ -2047,7 +2043,7 @@ msgstr "Spieler erfolgreich entfernt"
|
||||
msgid "Player Stats"
|
||||
msgstr "Spielerstatistiken"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:48
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
#: src/features/teams/components/team-profile/index.tsx:29
|
||||
msgid "Players"
|
||||
msgstr "Spieler"
|
||||
@@ -2083,7 +2079,7 @@ msgstr "Tipps"
|
||||
msgid "Predictions are open"
|
||||
msgstr "Tipps sind offen"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:201
|
||||
#: src/features/predictions/components/prediction-editor.tsx:202
|
||||
msgid "Previous match"
|
||||
msgstr "Vorheriges Spiel"
|
||||
|
||||
@@ -2095,7 +2091,7 @@ msgstr "Vorheriger Titel"
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
|
||||
#: src/features/badges/components/badge-showcase.tsx:301
|
||||
#: src/features/badges/components/badge-showcase.tsx:302
|
||||
msgid "Progress "
|
||||
msgstr "Fortschritt "
|
||||
|
||||
@@ -2116,13 +2112,13 @@ msgstr "Questlog"
|
||||
msgid "Re-roll"
|
||||
msgstr "Neu würfeln"
|
||||
|
||||
#: src/features/reactions/components/emoji-bar.tsx:213
|
||||
#: src/features/reactions/components/emoji-bar.tsx:210
|
||||
msgid "Reactions"
|
||||
msgstr "Reaktionen"
|
||||
|
||||
#. placeholder {0}: migrationStatus.playersProcessed
|
||||
#. placeholder {1}: migrationStatus.totalPlayers
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:72
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:75
|
||||
msgid "Recalculating badges in the background ({0}/{1})"
|
||||
msgstr "Abzeichen werden im Hintergrund neu berechnet ({0}/{1})"
|
||||
|
||||
@@ -2130,8 +2126,8 @@ msgstr "Abzeichen werden im Hintergrund neu berechnet ({0}/{1})"
|
||||
msgid "Recently finished"
|
||||
msgstr "Kürzlich beendet"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:79
|
||||
#: src/features/players/components/profile/index.tsx:37
|
||||
#: src/app/routes/_authed/stats.tsx:82
|
||||
#: src/features/players/components/profile/index.tsx:39
|
||||
msgid "Regional"
|
||||
msgstr "Regional"
|
||||
|
||||
@@ -2182,7 +2178,7 @@ msgid "rofl"
|
||||
msgstr "rofl"
|
||||
|
||||
#. placeholder {0}: match.round + 1
|
||||
#: src/features/matches/components/match-card.tsx:79
|
||||
#: src/features/matches/components/match-card.tsx:82
|
||||
msgid "Round {0}"
|
||||
msgstr "Runde {0}"
|
||||
|
||||
@@ -2245,8 +2241,8 @@ msgstr "Nach Songs suchen..."
|
||||
msgid "Search for teams to enroll..."
|
||||
msgstr "Teams zum Anmelden suchen..."
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:132
|
||||
#: src/features/players/components/player-stats-table.tsx:332
|
||||
#: src/features/players/components/league-head-to-head.tsx:137
|
||||
#: src/features/players/components/player-stats-table.tsx:331
|
||||
msgid "Search players"
|
||||
msgstr "Spieler suchen"
|
||||
|
||||
@@ -2283,7 +2279,7 @@ msgstr "Abzeichen auswählen"
|
||||
msgid "Select a device..."
|
||||
msgstr "Gerät auswählen..."
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:112
|
||||
#: src/features/players/components/league-head-to-head.tsx:117
|
||||
msgid "Select a player"
|
||||
msgstr "Wähle einen Spieler"
|
||||
|
||||
@@ -2366,7 +2362,7 @@ msgstr "Serverfunktionen"
|
||||
msgid "serverFn name"
|
||||
msgstr "serverFn-Name"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:60
|
||||
#: src/features/telemetry/components/usage-screen.tsx:212
|
||||
msgid "Sessions"
|
||||
msgstr "Sitzungen"
|
||||
@@ -2382,7 +2378,7 @@ msgstr "Details anzeigen"
|
||||
|
||||
#. placeholder {0}: filteredAndSortedStats.length
|
||||
#. placeholder {1}: playerStats.length
|
||||
#: src/features/players/components/player-stats-table.tsx:329
|
||||
#: src/features/players/components/player-stats-table.tsx:328
|
||||
msgid "Showing {0} of {1} players"
|
||||
msgstr "{0} von {1} Spielern angezeigt"
|
||||
|
||||
@@ -2434,7 +2430,7 @@ msgstr "Song-Segment darf höchstens 15 Sekunden lang sein"
|
||||
msgid "Song start time is required"
|
||||
msgstr "Startzeit des Songs ist erforderlich"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:350
|
||||
#: src/features/players/components/player-stats-table.tsx:349
|
||||
#: src/features/telemetry/components/shared/search-filter-bar.tsx:102
|
||||
msgid "Sort:"
|
||||
msgstr "Sortieren:"
|
||||
@@ -2489,21 +2485,21 @@ msgstr "Tippen, um zu suchen …"
|
||||
msgid "Starts in"
|
||||
msgstr "Beginnt in"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:382
|
||||
#: src/features/players/components/player-stats-table.tsx:381
|
||||
msgid "Stat abbreviations and MMR info"
|
||||
msgstr "Stat-Abkürzungen und MMR-Infos"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:389
|
||||
#: src/features/players/components/player-stats-table.tsx:388
|
||||
msgid "Stat Abbreviations:"
|
||||
msgstr "Stat-Abkürzungen:"
|
||||
|
||||
#: src/features/core/hooks/use-links.ts:17
|
||||
#: src/features/players/components/profile/index.tsx:33
|
||||
#: src/features/players/components/profile/index.tsx:35
|
||||
#: src/features/teams/components/team-profile/index.tsx:34
|
||||
msgid "Statistics"
|
||||
msgstr "Statistiken"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:52
|
||||
#: src/app/routes/_authed/stats.tsx:55
|
||||
msgid "Stats"
|
||||
msgstr "Stats"
|
||||
|
||||
@@ -2520,7 +2516,7 @@ msgstr "Status"
|
||||
#: src/components/sheet/slide-panel/slide-panel.tsx:40
|
||||
#: src/features/login/components/player-prompt/index.tsx:115
|
||||
#: src/features/login/components/player-prompt/index.tsx:126
|
||||
#: src/features/predictions/components/prediction-editor.tsx:240
|
||||
#: src/features/predictions/components/prediction-editor.tsx:244
|
||||
#: src/features/tournaments/components/edit-rules.tsx:39
|
||||
msgid "Submit"
|
||||
msgstr "Absenden"
|
||||
@@ -2543,7 +2539,7 @@ msgstr "System"
|
||||
msgid "T"
|
||||
msgstr "T"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:92
|
||||
#: src/features/players/components/league-head-to-head.tsx:97
|
||||
msgid "Tap an opponent below to compare"
|
||||
msgstr "Tippe unten auf einen Gegner zum Vergleichen"
|
||||
|
||||
@@ -2581,7 +2577,7 @@ msgstr "Team-Details"
|
||||
msgid "Team enrolled successfully!"
|
||||
msgstr "Team erfolgreich angemeldet!"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:93
|
||||
#: src/app/routes/api/teams/upload-logo.ts:94
|
||||
#: src/features/teams/components/team-profile/index.tsx:21
|
||||
msgid "Team not found"
|
||||
msgstr "Team nicht gefunden"
|
||||
@@ -2603,7 +2599,7 @@ msgstr "Team aktualisiert, aber Logo-Upload fehlgeschlagen: {0}"
|
||||
msgid "Team updated successfully!"
|
||||
msgstr "Team erfolgreich aktualisiert!"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:90
|
||||
#: src/features/players/components/profile/index.tsx:92
|
||||
#: src/features/players/components/profile/skeleton.tsx:75
|
||||
#: src/features/tournaments/components/profile/index.tsx:41
|
||||
#: src/features/tournaments/components/profile/skeleton.tsx:89
|
||||
@@ -2615,8 +2611,8 @@ msgstr "Teams"
|
||||
msgid "Teams created successfully!"
|
||||
msgstr "Teams erfolgreich erstellt!"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:20
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:55
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:26
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:57
|
||||
msgid "Telemetry"
|
||||
msgstr "Telemetrie"
|
||||
|
||||
@@ -2730,7 +2726,7 @@ msgstr "Turnier erstellt, aber Logo-Upload fehlgeschlagen: {0}"
|
||||
msgid "Tournament created successfully!"
|
||||
msgstr "Turnier erfolgreich erstellt!"
|
||||
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:94
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:95
|
||||
msgid "Tournament not found"
|
||||
msgstr "Turnier nicht gefunden"
|
||||
|
||||
@@ -2784,8 +2780,8 @@ msgstr "Team abmelden"
|
||||
msgid "Unknown error"
|
||||
msgstr "Unbekannter Fehler"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:142
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:140
|
||||
#: src/app/routes/api/teams/upload-logo.ts:143
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:141
|
||||
msgid "Unknown error occurred"
|
||||
msgstr "Unbekannter Fehler aufgetreten"
|
||||
|
||||
@@ -2810,7 +2806,7 @@ msgstr "Bevorstehend"
|
||||
msgid "Update Match"
|
||||
msgstr "Match aktualisieren"
|
||||
|
||||
#: src/features/players/components/profile/header.tsx:89
|
||||
#: src/features/players/components/profile/header.tsx:74
|
||||
msgid "Update Name"
|
||||
msgstr "Namen aktualisieren"
|
||||
|
||||
@@ -2824,7 +2820,7 @@ msgstr "Team aktualisieren"
|
||||
msgid "Update Tournament"
|
||||
msgstr "Turnier aktualisieren"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:45
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:51
|
||||
msgid "Usage"
|
||||
msgstr "Nutzung"
|
||||
|
||||
@@ -2837,7 +2833,7 @@ msgstr "User-Agent"
|
||||
msgid "versus"
|
||||
msgstr "gegen"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:73
|
||||
#: src/features/players/components/profile/index.tsx:75
|
||||
msgid "View all badges"
|
||||
msgstr "Alle Abzeichen ansehen"
|
||||
|
||||
@@ -2853,8 +2849,8 @@ msgstr "Turnierbaum ansehen"
|
||||
msgid "View Groups"
|
||||
msgstr "Gruppen ansehen"
|
||||
|
||||
#: src/features/bracket/components/match-dock.tsx:182
|
||||
#: src/features/matches/components/match-card.tsx:91
|
||||
#: src/features/bracket/components/match-dock.tsx:187
|
||||
#: src/features/matches/components/match-card.tsx:94
|
||||
msgid "View head-to-head"
|
||||
msgstr "Direktvergleich ansehen"
|
||||
|
||||
@@ -2935,7 +2931,7 @@ msgstr "Finale der Gewinnerrunde"
|
||||
|
||||
#: src/components/stats-overview.tsx:103
|
||||
#: src/components/stats-overview.tsx:136
|
||||
#: src/features/players/components/player-stats-table.tsx:366
|
||||
#: src/features/players/components/player-stats-table.tsx:365
|
||||
msgid "Wins"
|
||||
msgstr "Siege"
|
||||
|
||||
|
||||
+101
-105
@@ -13,7 +13,7 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/features/matches/components/match-card.tsx:80
|
||||
#: src/features/matches/components/match-card.tsx:83
|
||||
msgid " (Losers)"
|
||||
msgstr " (Losers)"
|
||||
|
||||
@@ -93,7 +93,7 @@ msgstr "{0, plural, one {player enrolled} other {players enrolled}}"
|
||||
|
||||
#. placeholder {0}: tournamentStart.toLocaleDateString(i18n.locale, { weekday: "short", month: "short", day: "numeric", })
|
||||
#. placeholder {1}: tournamentStart.toLocaleTimeString(i18n.locale, { hour: "2-digit", minute: "2-digit", })
|
||||
#: src/features/tournaments/components/started-tournament/header.tsx:51
|
||||
#: src/features/tournaments/components/started-tournament/header.tsx:56
|
||||
msgid "{0} at {1}"
|
||||
msgstr "{0} at {1}"
|
||||
|
||||
@@ -228,7 +228,7 @@ msgstr "{namesList} and 1 other"
|
||||
msgid "{size}-team knockout"
|
||||
msgstr "{size}-team knockout"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:446
|
||||
#: src/features/players/components/player-stats-table.tsx:445
|
||||
msgid "* Confidence penalty applied for players with <15 matches"
|
||||
msgstr "* Confidence penalty applied for players with <15 matches"
|
||||
|
||||
@@ -241,7 +241,7 @@ msgstr "* If necessary"
|
||||
msgid "* PICKS shows correct picks / total picks made"
|
||||
msgstr "* PICKS shows correct picks / total picks made"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:449
|
||||
#: src/features/players/components/player-stats-table.tsx:448
|
||||
msgid "** Not an official rating"
|
||||
msgstr "** Not an official rating"
|
||||
|
||||
@@ -254,19 +254,19 @@ msgstr "&"
|
||||
msgid "#{0}"
|
||||
msgstr "#{0}"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:416
|
||||
#: src/features/players/components/player-stats-table.tsx:415
|
||||
msgid "• <0>AC:</0> Average Cups Per Match"
|
||||
msgstr "• <0>AC:</0> Average Cups Per Match"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:419
|
||||
#: src/features/players/components/player-stats-table.tsx:418
|
||||
msgid "• <0>ACA:</0> Average Cups Against"
|
||||
msgstr "• <0>ACA:</0> Average Cups Against"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:413
|
||||
#: src/features/players/components/player-stats-table.tsx:412
|
||||
msgid "• <0>ALM:</0> Average Loss Margin"
|
||||
msgstr "• <0>ALM:</0> Average Loss Margin"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:410
|
||||
#: src/features/players/components/player-stats-table.tsx:409
|
||||
msgid "• <0>AWM:</0> Average Win Margin"
|
||||
msgstr "• <0>AWM:</0> Average Win Margin"
|
||||
|
||||
@@ -274,15 +274,15 @@ msgstr "• <0>AWM:</0> Average Win Margin"
|
||||
msgid "• <0>Bracket reset:</0> only picked if your bracket triggers it — worth double the Final"
|
||||
msgstr "• <0>Bracket reset:</0> only picked if your bracket triggers it — worth double the Final"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:425
|
||||
#: src/features/players/components/player-stats-table.tsx:424
|
||||
msgid "• <0>CA:</0> Cups Against"
|
||||
msgstr "• <0>CA:</0> Cups Against"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:422
|
||||
#: src/features/players/components/player-stats-table.tsx:421
|
||||
msgid "• <0>CF:</0> Cups For"
|
||||
msgstr "• <0>CF:</0> Cups For"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:404
|
||||
#: src/features/players/components/player-stats-table.tsx:403
|
||||
msgid "• <0>L:</0> Losses"
|
||||
msgstr "• <0>L:</0> Losses"
|
||||
|
||||
@@ -290,23 +290,23 @@ msgstr "• <0>L:</0> Losses"
|
||||
msgid "• <0>Losers bracket:</0> 5, 10, 20, 40…"
|
||||
msgstr "• <0>Losers bracket:</0> 5, 10, 20, 40…"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:392
|
||||
#: src/features/players/components/player-stats-table.tsx:391
|
||||
msgid "• <0>M:</0> Matches"
|
||||
msgstr "• <0>M:</0> Matches"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:398
|
||||
#: src/features/players/components/player-stats-table.tsx:397
|
||||
msgid "• <0>MMR:</0> Matchmaking Rating"
|
||||
msgstr "• <0>MMR:</0> Matchmaking Rating"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:395
|
||||
#: src/features/players/components/player-stats-table.tsx:394
|
||||
msgid "• <0>T:</0> Tournaments"
|
||||
msgstr "• <0>T:</0> Tournaments"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:401
|
||||
#: src/features/players/components/player-stats-table.tsx:400
|
||||
msgid "• <0>W:</0> Wins"
|
||||
msgstr "• <0>W:</0> Wins"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:407
|
||||
#: src/features/players/components/player-stats-table.tsx:406
|
||||
msgid "• <0>W%:</0> Win Percentage"
|
||||
msgstr "• <0>W%:</0> Win Percentage"
|
||||
|
||||
@@ -314,11 +314,11 @@ msgstr "• <0>W%:</0> Win Percentage"
|
||||
msgid "• <0>Winners bracket:</0> 10, 20, 40, 80…"
|
||||
msgstr "• <0>Winners bracket:</0> 10, 20, 40, 80…"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:437
|
||||
#: src/features/players/components/player-stats-table.tsx:436
|
||||
msgid "• Average Cups/Match (25%)"
|
||||
msgstr "• Average Cups/Match (25%)"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:440
|
||||
#: src/features/players/components/player-stats-table.tsx:439
|
||||
msgid "• Average Win Margin (15%)"
|
||||
msgstr "• Average Win Margin (15%)"
|
||||
|
||||
@@ -326,11 +326,11 @@ msgstr "• Average Win Margin (15%)"
|
||||
msgid "• Each correct pick earns points, doubling every round"
|
||||
msgstr "• Each correct pick earns points, doubling every round"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:443
|
||||
#: src/features/players/components/player-stats-table.tsx:442
|
||||
msgid "• Match Volume Bonus (10%)"
|
||||
msgstr "• Match Volume Bonus (10%)"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:434
|
||||
#: src/features/players/components/player-stats-table.tsx:433
|
||||
msgid "• Win Rate (50%)"
|
||||
msgstr "• Win Rate (50%)"
|
||||
|
||||
@@ -457,9 +457,9 @@ msgstr "Admin"
|
||||
msgid "Aero"
|
||||
msgstr "Aero"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:65
|
||||
#: src/app/routes/_authed/stats.tsx:68
|
||||
#: src/features/activities/components/activities-table.tsx:92
|
||||
#: src/features/players/components/profile/index.tsx:35
|
||||
#: src/features/players/components/profile/index.tsx:37
|
||||
msgid "All"
|
||||
msgstr "All"
|
||||
|
||||
@@ -572,7 +572,7 @@ msgstr "Avg Win Margin"
|
||||
msgid "Award Badge"
|
||||
msgstr "Award Badge"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:60
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:63
|
||||
msgid "Award Badges"
|
||||
msgstr "Award Badges"
|
||||
|
||||
@@ -606,15 +606,15 @@ msgstr "Back"
|
||||
msgid "Badge awarded to {playerName}"
|
||||
msgstr "Badge awarded to {playerName}"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:91
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:94
|
||||
msgid "Badge recalculation failed. Check the server logs."
|
||||
msgstr "Badge recalculation failed. Check the server logs."
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:39
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:40
|
||||
msgid "Badge recalculation is already running"
|
||||
msgstr "Badge recalculation is already running"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:38
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:39
|
||||
msgid "Badge recalculation started in the background"
|
||||
msgstr "Badge recalculation started in the background"
|
||||
|
||||
@@ -622,7 +622,7 @@ msgstr "Badge recalculation started in the background"
|
||||
msgid "Badge Type"
|
||||
msgstr "Badge Type"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:66
|
||||
#: src/features/players/components/profile/index.tsx:68
|
||||
msgid "Badges"
|
||||
msgstr "Badges"
|
||||
|
||||
@@ -645,7 +645,7 @@ msgid "Bracket"
|
||||
msgstr "Bracket"
|
||||
|
||||
#: src/app/routes/_authed/admin/preview.tsx:13
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:102
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:98
|
||||
msgid "Bracket Preview"
|
||||
msgstr "Bracket Preview"
|
||||
|
||||
@@ -720,7 +720,7 @@ msgstr "Choose a player"
|
||||
msgid "Claymation"
|
||||
msgstr "Claymation"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:102
|
||||
#: src/features/players/components/league-head-to-head.tsx:107
|
||||
msgid "Clear selected player"
|
||||
msgstr "Clear selected player"
|
||||
|
||||
@@ -1118,7 +1118,7 @@ msgstr "Error spikes and failing functions, pushed to this account"
|
||||
msgid "Error: {errorMessage}"
|
||||
msgstr "Error: {errorMessage}"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:57
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:63
|
||||
#: src/features/telemetry/components/usage-screen.tsx:260
|
||||
msgid "Errors"
|
||||
msgstr "Errors"
|
||||
@@ -1186,7 +1186,7 @@ msgstr "Failed to set active device"
|
||||
msgid "Failed to set volume"
|
||||
msgstr "Failed to set volume"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:42
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:43
|
||||
msgid "Failed to start badge recalculation"
|
||||
msgstr "Failed to start badge recalculation"
|
||||
|
||||
@@ -1198,8 +1198,8 @@ msgstr "Failed to update team"
|
||||
msgid "Failed to update tournament"
|
||||
msgstr "Failed to update tournament"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:141
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:139
|
||||
#: src/app/routes/api/teams/upload-logo.ts:142
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:140
|
||||
#: src/features/teams/components/team-form/index.tsx:148
|
||||
#: src/features/tournaments/components/tournament-form.tsx:88
|
||||
msgid "Failed to upload logo"
|
||||
@@ -1209,8 +1209,8 @@ msgstr "Failed to upload logo"
|
||||
msgid "Failure"
|
||||
msgstr "Failure"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:80
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:81
|
||||
#: src/app/routes/api/teams/upload-logo.ts:81
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:82
|
||||
msgid "File too large. Maximum size is 10MB."
|
||||
msgstr "File too large. Maximum size is 10MB."
|
||||
|
||||
@@ -1264,7 +1264,7 @@ msgstr "Function"
|
||||
msgid "Function Name"
|
||||
msgstr "Function Name"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:51
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:57
|
||||
msgid "Functions"
|
||||
msgstr "Functions"
|
||||
|
||||
@@ -1333,13 +1333,13 @@ msgstr "have fun, good luck!"
|
||||
msgid "Have you played before?"
|
||||
msgstr "Have you played before?"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:53
|
||||
#: src/app/routes/_authed/stats.tsx:56
|
||||
#: src/features/bracket/components/match-dock.tsx:174
|
||||
#: src/features/bracket/components/match-dock.tsx:219
|
||||
#: src/features/matches/components/match-card.tsx:86
|
||||
#: src/features/matches/components/match-card.tsx:230
|
||||
#: src/features/players/components/league-head-to-head.tsx:193
|
||||
#: src/features/players/components/profile/header.tsx:94
|
||||
#: src/features/bracket/components/match-dock.tsx:203
|
||||
#: src/features/matches/components/match-card.tsx:89
|
||||
#: src/features/matches/components/match-card.tsx:214
|
||||
#: src/features/players/components/league-head-to-head.tsx:198
|
||||
#: src/features/players/components/profile/header.tsx:79
|
||||
msgid "Head to Head"
|
||||
msgstr "Head to Head"
|
||||
|
||||
@@ -1401,8 +1401,8 @@ msgstr "Incorrect code"
|
||||
msgid "Invalid action"
|
||||
msgstr "Invalid action"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:66
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:67
|
||||
#: src/app/routes/api/teams/upload-logo.ts:67
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:68
|
||||
msgid "Invalid file type. Only JPEG, PNG and GIF are allowed."
|
||||
msgstr "Invalid file type. Only JPEG, PNG and GIF are allowed."
|
||||
|
||||
@@ -1410,8 +1410,8 @@ msgstr "Invalid file type. Only JPEG, PNG and GIF are allowed."
|
||||
msgid "Invalid hex color format"
|
||||
msgstr "Invalid hex color format"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:34
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:35
|
||||
#: src/app/routes/api/teams/upload-logo.ts:35
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:36
|
||||
msgid "Invalid input"
|
||||
msgstr "Invalid input"
|
||||
|
||||
@@ -1470,7 +1470,7 @@ msgid "Last name must be 3-20 characters long and contain only letters"
|
||||
msgstr "Last name must be 3-20 characters long and contain only letters"
|
||||
|
||||
#. placeholder {0}: migrationStatus.result.totalBadgesEarned
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:79
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:82
|
||||
msgid "Last recalculation: {0, plural, one {# badge earned} other {# badges earned}}"
|
||||
msgstr "Last recalculation: {0, plural, one {# badge earned} other {# badges earned}}"
|
||||
|
||||
@@ -1504,8 +1504,8 @@ msgstr "Location is required"
|
||||
msgid "Logo"
|
||||
msgstr "Logo"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:47
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:48
|
||||
#: src/app/routes/api/teams/upload-logo.ts:48
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:49
|
||||
msgid "Logo file is required"
|
||||
msgstr "Logo file is required"
|
||||
|
||||
@@ -1530,8 +1530,8 @@ msgstr "Losses"
|
||||
msgid "M"
|
||||
msgstr "M"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:72
|
||||
#: src/features/players/components/profile/index.tsx:36
|
||||
#: src/app/routes/_authed/stats.tsx:75
|
||||
#: src/features/players/components/profile/index.tsx:38
|
||||
msgid "Mainline"
|
||||
msgstr "Mainline"
|
||||
|
||||
@@ -1559,7 +1559,7 @@ msgid "Manage Team Songs/Logos"
|
||||
msgstr "Manage Team Songs/Logos"
|
||||
|
||||
#: src/app/routes/_authed/admin/tournaments/index.tsx:17
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:50
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:51
|
||||
msgid "Manage Tournaments"
|
||||
msgstr "Manage Tournaments"
|
||||
|
||||
@@ -1611,8 +1611,8 @@ msgstr "Match History ({totalMatches})"
|
||||
msgid "Match started!"
|
||||
msgstr "Match started!"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:376
|
||||
#: src/features/players/components/profile/index.tsx:85
|
||||
#: src/features/players/components/player-stats-table.tsx:375
|
||||
#: src/features/players/components/profile/index.tsx:87
|
||||
#: src/features/players/components/profile/skeleton.tsx:70
|
||||
#: src/features/teams/components/team-profile/index.tsx:44
|
||||
#: src/features/teams/components/team-profile/skeleton.tsx:75
|
||||
@@ -1621,7 +1621,7 @@ msgstr "Match started!"
|
||||
msgid "Matches"
|
||||
msgstr "Matches"
|
||||
|
||||
#: src/features/matches/components/match-list.tsx:33
|
||||
#: src/features/matches/components/match-list.tsx:34
|
||||
msgid "Matches for regionals are unordered"
|
||||
msgstr "Matches for regionals are unordered"
|
||||
|
||||
@@ -1630,11 +1630,11 @@ msgstr "Matches for regionals are unordered"
|
||||
msgid "Matches Played"
|
||||
msgstr "Matches Played"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:249
|
||||
#: src/features/predictions/components/prediction-editor.tsx:253
|
||||
msgid "Matchup"
|
||||
msgstr "Matchup"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:226
|
||||
#: src/features/predictions/components/prediction-editor.tsx:230
|
||||
msgid "Matchup details"
|
||||
msgstr "Matchup details"
|
||||
|
||||
@@ -1642,20 +1642,20 @@ msgstr "Matchup details"
|
||||
msgid "MAU"
|
||||
msgstr "MAU"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:123
|
||||
#: src/features/players/components/league-head-to-head.tsx:128
|
||||
msgid "Me"
|
||||
msgstr "Me"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:65
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:68
|
||||
msgid "Migrate Badge Progress"
|
||||
msgstr "Migrate Badge Progress"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:131
|
||||
#: src/features/players/components/player-stats-table.tsx:356
|
||||
#: src/features/players/components/player-stats-table.tsx:355
|
||||
msgid "MMR"
|
||||
msgstr "MMR"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:431
|
||||
#: src/features/players/components/player-stats-table.tsx:430
|
||||
msgid "MMR Calculation:"
|
||||
msgstr "MMR Calculation:"
|
||||
|
||||
@@ -1712,7 +1712,7 @@ msgstr "New client error"
|
||||
msgid "New Player"
|
||||
msgstr "New Player"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:211
|
||||
#: src/features/predictions/components/prediction-editor.tsx:212
|
||||
msgid "Next match"
|
||||
msgstr "Next match"
|
||||
|
||||
@@ -1775,7 +1775,7 @@ msgstr "No matches scheduled"
|
||||
msgid "No player activity found"
|
||||
msgstr "No player activity found"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:151
|
||||
#: src/features/players/components/league-head-to-head.tsx:156
|
||||
msgid "No players available"
|
||||
msgstr "No players available"
|
||||
|
||||
@@ -1784,8 +1784,8 @@ msgstr "No players available"
|
||||
msgid "No players enrolled yet"
|
||||
msgstr "No players enrolled yet"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:151
|
||||
#: src/features/players/components/player-stats-table.tsx:486
|
||||
#: src/features/players/components/league-head-to-head.tsx:156
|
||||
#: src/features/players/components/player-stats-table.tsx:485
|
||||
msgid "No players found matching \"{search}\""
|
||||
msgstr "No players found matching \"{search}\""
|
||||
|
||||
@@ -1818,7 +1818,7 @@ msgstr "No song selected"
|
||||
msgid "No state captured"
|
||||
msgstr "No state captured"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:319
|
||||
#: src/features/players/components/player-stats-table.tsx:318
|
||||
msgid "No Stats Available"
|
||||
msgstr "No Stats Available"
|
||||
|
||||
@@ -1843,7 +1843,7 @@ msgstr "No teams enrolled yet"
|
||||
msgid "No teams found matching \"{search}\""
|
||||
msgstr "No teams found matching \"{search}\""
|
||||
|
||||
#: src/features/tournaments/components/tournament-list.tsx:95
|
||||
#: src/features/tournaments/components/tournament-list.tsx:98
|
||||
msgid "No tournaments found"
|
||||
msgstr "No tournaments found"
|
||||
|
||||
@@ -1908,10 +1908,6 @@ msgstr "Once enrolled, you can view other free agents and their phone number in
|
||||
msgid "One time code"
|
||||
msgstr "One time code"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:95
|
||||
msgid "Open Pocketbase"
|
||||
msgstr "Open Pocketbase"
|
||||
|
||||
#: src/features/bracket/components/match-report.tsx:273
|
||||
msgid "Opponent"
|
||||
msgstr "Opponent"
|
||||
@@ -1937,7 +1933,7 @@ msgstr "Other players' predictions are hidden until the tournament starts."
|
||||
msgid "Overtime count is required"
|
||||
msgstr "Overtime count is required"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:61
|
||||
#: src/features/players/components/profile/index.tsx:63
|
||||
#: src/features/players/components/profile/skeleton.tsx:65
|
||||
#: src/features/teams/components/team-profile/index.tsx:25
|
||||
#: src/features/teams/components/team-profile/skeleton.tsx:70
|
||||
@@ -2047,7 +2043,7 @@ msgstr "Player removed successfully"
|
||||
msgid "Player Stats"
|
||||
msgstr "Player Stats"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:48
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
#: src/features/teams/components/team-profile/index.tsx:29
|
||||
msgid "Players"
|
||||
msgstr "Players"
|
||||
@@ -2083,7 +2079,7 @@ msgstr "Predictions"
|
||||
msgid "Predictions are open"
|
||||
msgstr "Predictions are open"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:201
|
||||
#: src/features/predictions/components/prediction-editor.tsx:202
|
||||
msgid "Previous match"
|
||||
msgstr "Previous match"
|
||||
|
||||
@@ -2095,7 +2091,7 @@ msgstr "Previous track"
|
||||
msgid "Profile"
|
||||
msgstr "Profile"
|
||||
|
||||
#: src/features/badges/components/badge-showcase.tsx:301
|
||||
#: src/features/badges/components/badge-showcase.tsx:302
|
||||
msgid "Progress "
|
||||
msgstr "Progress "
|
||||
|
||||
@@ -2116,13 +2112,13 @@ msgstr "Quest Log"
|
||||
msgid "Re-roll"
|
||||
msgstr "Re-roll"
|
||||
|
||||
#: src/features/reactions/components/emoji-bar.tsx:213
|
||||
#: src/features/reactions/components/emoji-bar.tsx:210
|
||||
msgid "Reactions"
|
||||
msgstr "Reactions"
|
||||
|
||||
#. placeholder {0}: migrationStatus.playersProcessed
|
||||
#. placeholder {1}: migrationStatus.totalPlayers
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:72
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:75
|
||||
msgid "Recalculating badges in the background ({0}/{1})"
|
||||
msgstr "Recalculating badges in the background ({0}/{1})"
|
||||
|
||||
@@ -2130,8 +2126,8 @@ msgstr "Recalculating badges in the background ({0}/{1})"
|
||||
msgid "Recently finished"
|
||||
msgstr "Recently finished"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:79
|
||||
#: src/features/players/components/profile/index.tsx:37
|
||||
#: src/app/routes/_authed/stats.tsx:82
|
||||
#: src/features/players/components/profile/index.tsx:39
|
||||
msgid "Regional"
|
||||
msgstr "Regional"
|
||||
|
||||
@@ -2182,7 +2178,7 @@ msgid "rofl"
|
||||
msgstr "rofl"
|
||||
|
||||
#. placeholder {0}: match.round + 1
|
||||
#: src/features/matches/components/match-card.tsx:79
|
||||
#: src/features/matches/components/match-card.tsx:82
|
||||
msgid "Round {0}"
|
||||
msgstr "Round {0}"
|
||||
|
||||
@@ -2245,8 +2241,8 @@ msgstr "Search for songs..."
|
||||
msgid "Search for teams to enroll..."
|
||||
msgstr "Search for teams to enroll..."
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:132
|
||||
#: src/features/players/components/player-stats-table.tsx:332
|
||||
#: src/features/players/components/league-head-to-head.tsx:137
|
||||
#: src/features/players/components/player-stats-table.tsx:331
|
||||
msgid "Search players"
|
||||
msgstr "Search players"
|
||||
|
||||
@@ -2283,7 +2279,7 @@ msgstr "Select a badge"
|
||||
msgid "Select a device..."
|
||||
msgstr "Select a device..."
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:112
|
||||
#: src/features/players/components/league-head-to-head.tsx:117
|
||||
msgid "Select a player"
|
||||
msgstr "Select a player"
|
||||
|
||||
@@ -2366,7 +2362,7 @@ msgstr "Server functions"
|
||||
msgid "serverFn name"
|
||||
msgstr "serverFn name"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:60
|
||||
#: src/features/telemetry/components/usage-screen.tsx:212
|
||||
msgid "Sessions"
|
||||
msgstr "Sessions"
|
||||
@@ -2382,7 +2378,7 @@ msgstr "Show details"
|
||||
|
||||
#. placeholder {0}: filteredAndSortedStats.length
|
||||
#. placeholder {1}: playerStats.length
|
||||
#: src/features/players/components/player-stats-table.tsx:329
|
||||
#: src/features/players/components/player-stats-table.tsx:328
|
||||
msgid "Showing {0} of {1} players"
|
||||
msgstr "Showing {0} of {1} players"
|
||||
|
||||
@@ -2434,7 +2430,7 @@ msgstr "Song segment must be no more than 15 seconds"
|
||||
msgid "Song start time is required"
|
||||
msgstr "Song start time is required"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:350
|
||||
#: src/features/players/components/player-stats-table.tsx:349
|
||||
#: src/features/telemetry/components/shared/search-filter-bar.tsx:102
|
||||
msgid "Sort:"
|
||||
msgstr "Sort:"
|
||||
@@ -2489,21 +2485,21 @@ msgstr "Start typing to search..."
|
||||
msgid "Starts in"
|
||||
msgstr "Starts in"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:382
|
||||
#: src/features/players/components/player-stats-table.tsx:381
|
||||
msgid "Stat abbreviations and MMR info"
|
||||
msgstr "Stat abbreviations and MMR info"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:389
|
||||
#: src/features/players/components/player-stats-table.tsx:388
|
||||
msgid "Stat Abbreviations:"
|
||||
msgstr "Stat Abbreviations:"
|
||||
|
||||
#: src/features/core/hooks/use-links.ts:17
|
||||
#: src/features/players/components/profile/index.tsx:33
|
||||
#: src/features/players/components/profile/index.tsx:35
|
||||
#: src/features/teams/components/team-profile/index.tsx:34
|
||||
msgid "Statistics"
|
||||
msgstr "Statistics"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:52
|
||||
#: src/app/routes/_authed/stats.tsx:55
|
||||
msgid "Stats"
|
||||
msgstr "Stats"
|
||||
|
||||
@@ -2520,7 +2516,7 @@ msgstr "Status"
|
||||
#: src/components/sheet/slide-panel/slide-panel.tsx:40
|
||||
#: src/features/login/components/player-prompt/index.tsx:115
|
||||
#: src/features/login/components/player-prompt/index.tsx:126
|
||||
#: src/features/predictions/components/prediction-editor.tsx:240
|
||||
#: src/features/predictions/components/prediction-editor.tsx:244
|
||||
#: src/features/tournaments/components/edit-rules.tsx:39
|
||||
msgid "Submit"
|
||||
msgstr "Submit"
|
||||
@@ -2543,7 +2539,7 @@ msgstr "System"
|
||||
msgid "T"
|
||||
msgstr "T"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:92
|
||||
#: src/features/players/components/league-head-to-head.tsx:97
|
||||
msgid "Tap an opponent below to compare"
|
||||
msgstr "Tap an opponent below to compare"
|
||||
|
||||
@@ -2581,7 +2577,7 @@ msgstr "Team Details"
|
||||
msgid "Team enrolled successfully!"
|
||||
msgstr "Team enrolled successfully!"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:93
|
||||
#: src/app/routes/api/teams/upload-logo.ts:94
|
||||
#: src/features/teams/components/team-profile/index.tsx:21
|
||||
msgid "Team not found"
|
||||
msgstr "Team not found"
|
||||
@@ -2603,7 +2599,7 @@ msgstr "Team updated but logo upload failed: {0}"
|
||||
msgid "Team updated successfully!"
|
||||
msgstr "Team updated successfully!"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:90
|
||||
#: src/features/players/components/profile/index.tsx:92
|
||||
#: src/features/players/components/profile/skeleton.tsx:75
|
||||
#: src/features/tournaments/components/profile/index.tsx:41
|
||||
#: src/features/tournaments/components/profile/skeleton.tsx:89
|
||||
@@ -2615,8 +2611,8 @@ msgstr "Teams"
|
||||
msgid "Teams created successfully!"
|
||||
msgstr "Teams created successfully!"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:20
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:55
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:26
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:57
|
||||
msgid "Telemetry"
|
||||
msgstr "Telemetry"
|
||||
|
||||
@@ -2730,7 +2726,7 @@ msgstr "Tournament created but logo upload failed: {0}"
|
||||
msgid "Tournament created successfully!"
|
||||
msgstr "Tournament created successfully!"
|
||||
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:94
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:95
|
||||
msgid "Tournament not found"
|
||||
msgstr "Tournament not found"
|
||||
|
||||
@@ -2784,8 +2780,8 @@ msgstr "Unenroll Team"
|
||||
msgid "Unknown error"
|
||||
msgstr "Unknown error"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:142
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:140
|
||||
#: src/app/routes/api/teams/upload-logo.ts:143
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:141
|
||||
msgid "Unknown error occurred"
|
||||
msgstr "Unknown error occurred"
|
||||
|
||||
@@ -2810,7 +2806,7 @@ msgstr "Upcoming"
|
||||
msgid "Update Match"
|
||||
msgstr "Update Match"
|
||||
|
||||
#: src/features/players/components/profile/header.tsx:89
|
||||
#: src/features/players/components/profile/header.tsx:74
|
||||
msgid "Update Name"
|
||||
msgstr "Update Name"
|
||||
|
||||
@@ -2824,7 +2820,7 @@ msgstr "Update Team"
|
||||
msgid "Update Tournament"
|
||||
msgstr "Update Tournament"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:45
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:51
|
||||
msgid "Usage"
|
||||
msgstr "Usage"
|
||||
|
||||
@@ -2837,7 +2833,7 @@ msgstr "User Agent"
|
||||
msgid "versus"
|
||||
msgstr "versus"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:73
|
||||
#: src/features/players/components/profile/index.tsx:75
|
||||
msgid "View all badges"
|
||||
msgstr "View all badges"
|
||||
|
||||
@@ -2853,8 +2849,8 @@ msgstr "View Bracket"
|
||||
msgid "View Groups"
|
||||
msgstr "View Groups"
|
||||
|
||||
#: src/features/bracket/components/match-dock.tsx:182
|
||||
#: src/features/matches/components/match-card.tsx:91
|
||||
#: src/features/bracket/components/match-dock.tsx:187
|
||||
#: src/features/matches/components/match-card.tsx:94
|
||||
msgid "View head-to-head"
|
||||
msgstr "View head-to-head"
|
||||
|
||||
@@ -2935,7 +2931,7 @@ msgstr "Winners Bracket Final"
|
||||
|
||||
#: src/components/stats-overview.tsx:103
|
||||
#: src/components/stats-overview.tsx:136
|
||||
#: src/features/players/components/player-stats-table.tsx:366
|
||||
#: src/features/players/components/player-stats-table.tsx:365
|
||||
msgid "Wins"
|
||||
msgstr "Wins"
|
||||
|
||||
|
||||
+101
-105
@@ -13,7 +13,7 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/features/matches/components/match-card.tsx:80
|
||||
#: src/features/matches/components/match-card.tsx:83
|
||||
msgid " (Losers)"
|
||||
msgstr " (Perdedores)"
|
||||
|
||||
@@ -93,7 +93,7 @@ msgstr "{0, plural, one {jugador inscrito} other {jugadores inscritos}}"
|
||||
|
||||
#. placeholder {0}: tournamentStart.toLocaleDateString(i18n.locale, { weekday: "short", month: "short", day: "numeric", })
|
||||
#. placeholder {1}: tournamentStart.toLocaleTimeString(i18n.locale, { hour: "2-digit", minute: "2-digit", })
|
||||
#: src/features/tournaments/components/started-tournament/header.tsx:51
|
||||
#: src/features/tournaments/components/started-tournament/header.tsx:56
|
||||
msgid "{0} at {1}"
|
||||
msgstr "{0} a las {1}"
|
||||
|
||||
@@ -228,7 +228,7 @@ msgstr "{namesList} y 1 más"
|
||||
msgid "{size}-team knockout"
|
||||
msgstr "Eliminación directa de {size} equipos"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:446
|
||||
#: src/features/players/components/player-stats-table.tsx:445
|
||||
msgid "* Confidence penalty applied for players with <15 matches"
|
||||
msgstr "* Se aplica una penalización de confianza a jugadores con menos de 15 partidos"
|
||||
|
||||
@@ -241,7 +241,7 @@ msgstr "* Si es necesario"
|
||||
msgid "* PICKS shows correct picks / total picks made"
|
||||
msgstr "* PICKS muestra aciertos / total de pronósticos realizados"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:449
|
||||
#: src/features/players/components/player-stats-table.tsx:448
|
||||
msgid "** Not an official rating"
|
||||
msgstr "** No es una calificación oficial"
|
||||
|
||||
@@ -254,19 +254,19 @@ msgstr "&"
|
||||
msgid "#{0}"
|
||||
msgstr "#{0}"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:416
|
||||
#: src/features/players/components/player-stats-table.tsx:415
|
||||
msgid "• <0>AC:</0> Average Cups Per Match"
|
||||
msgstr "• <0>AC:</0> Promedio de Vasos por Partido"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:419
|
||||
#: src/features/players/components/player-stats-table.tsx:418
|
||||
msgid "• <0>ACA:</0> Average Cups Against"
|
||||
msgstr "• <0>ACA:</0> Promedio de Vasos en Contra"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:413
|
||||
#: src/features/players/components/player-stats-table.tsx:412
|
||||
msgid "• <0>ALM:</0> Average Loss Margin"
|
||||
msgstr "• <0>ALM:</0> Margen Promedio de Derrota"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:410
|
||||
#: src/features/players/components/player-stats-table.tsx:409
|
||||
msgid "• <0>AWM:</0> Average Win Margin"
|
||||
msgstr "• <0>AWM:</0> Margen Promedio de Victoria"
|
||||
|
||||
@@ -274,15 +274,15 @@ msgstr "• <0>AWM:</0> Margen Promedio de Victoria"
|
||||
msgid "• <0>Bracket reset:</0> only picked if your bracket triggers it — worth double the Final"
|
||||
msgstr "• <0>Reinicio de bracket:</0> solo se selecciona si tu bracket lo activa — vale el doble de la Final"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:425
|
||||
#: src/features/players/components/player-stats-table.tsx:424
|
||||
msgid "• <0>CA:</0> Cups Against"
|
||||
msgstr "• <0>VC:</0> Vasos en Contra"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:422
|
||||
#: src/features/players/components/player-stats-table.tsx:421
|
||||
msgid "• <0>CF:</0> Cups For"
|
||||
msgstr "• <0>VF:</0> Vasos a Favor"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:404
|
||||
#: src/features/players/components/player-stats-table.tsx:403
|
||||
msgid "• <0>L:</0> Losses"
|
||||
msgstr "• <0>D:</0> Derrotas"
|
||||
|
||||
@@ -290,23 +290,23 @@ msgstr "• <0>D:</0> Derrotas"
|
||||
msgid "• <0>Losers bracket:</0> 5, 10, 20, 40…"
|
||||
msgstr "• <0>Bracket de perdedores:</0> 5, 10, 20, 40…"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:392
|
||||
#: src/features/players/components/player-stats-table.tsx:391
|
||||
msgid "• <0>M:</0> Matches"
|
||||
msgstr "• <0>J:</0> Partidos"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:398
|
||||
#: src/features/players/components/player-stats-table.tsx:397
|
||||
msgid "• <0>MMR:</0> Matchmaking Rating"
|
||||
msgstr "• <0>MMR:</0> Calificación de Emparejamiento"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:395
|
||||
#: src/features/players/components/player-stats-table.tsx:394
|
||||
msgid "• <0>T:</0> Tournaments"
|
||||
msgstr "• <0>T:</0> Torneos"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:401
|
||||
#: src/features/players/components/player-stats-table.tsx:400
|
||||
msgid "• <0>W:</0> Wins"
|
||||
msgstr "• <0>V:</0> Victorias"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:407
|
||||
#: src/features/players/components/player-stats-table.tsx:406
|
||||
msgid "• <0>W%:</0> Win Percentage"
|
||||
msgstr "• <0>V%:</0> Porcentaje de Victorias"
|
||||
|
||||
@@ -314,11 +314,11 @@ msgstr "• <0>V%:</0> Porcentaje de Victorias"
|
||||
msgid "• <0>Winners bracket:</0> 10, 20, 40, 80…"
|
||||
msgstr "• <0>Bracket de ganadores:</0> 10, 20, 40, 80…"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:437
|
||||
#: src/features/players/components/player-stats-table.tsx:436
|
||||
msgid "• Average Cups/Match (25%)"
|
||||
msgstr "• Promedio de Vasos/Partido (25%)"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:440
|
||||
#: src/features/players/components/player-stats-table.tsx:439
|
||||
msgid "• Average Win Margin (15%)"
|
||||
msgstr "• Margen Promedio de Victoria (15%)"
|
||||
|
||||
@@ -326,11 +326,11 @@ msgstr "• Margen Promedio de Victoria (15%)"
|
||||
msgid "• Each correct pick earns points, doubling every round"
|
||||
msgstr "• Cada acierto suma puntos, y se duplican cada ronda"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:443
|
||||
#: src/features/players/components/player-stats-table.tsx:442
|
||||
msgid "• Match Volume Bonus (10%)"
|
||||
msgstr "• Bono por Volumen de Partidos (10%)"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:434
|
||||
#: src/features/players/components/player-stats-table.tsx:433
|
||||
msgid "• Win Rate (50%)"
|
||||
msgstr "• Porcentaje de Victorias (50%)"
|
||||
|
||||
@@ -457,9 +457,9 @@ msgstr "Admin"
|
||||
msgid "Aero"
|
||||
msgstr "Aero"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:65
|
||||
#: src/app/routes/_authed/stats.tsx:68
|
||||
#: src/features/activities/components/activities-table.tsx:92
|
||||
#: src/features/players/components/profile/index.tsx:35
|
||||
#: src/features/players/components/profile/index.tsx:37
|
||||
msgid "All"
|
||||
msgstr "Todos"
|
||||
|
||||
@@ -572,7 +572,7 @@ msgstr "Prom. Margen de Victoria"
|
||||
msgid "Award Badge"
|
||||
msgstr "Otorgar insignia"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:60
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:63
|
||||
msgid "Award Badges"
|
||||
msgstr "Otorgar insignias"
|
||||
|
||||
@@ -606,15 +606,15 @@ msgstr "Atrás"
|
||||
msgid "Badge awarded to {playerName}"
|
||||
msgstr "Insignia otorgada a {playerName}"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:91
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:94
|
||||
msgid "Badge recalculation failed. Check the server logs."
|
||||
msgstr "Falló el recálculo de insignias. Revisa los registros del servidor."
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:39
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:40
|
||||
msgid "Badge recalculation is already running"
|
||||
msgstr "El recálculo de insignias ya está en curso"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:38
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:39
|
||||
msgid "Badge recalculation started in the background"
|
||||
msgstr "Recálculo de insignias iniciado en segundo plano"
|
||||
|
||||
@@ -622,7 +622,7 @@ msgstr "Recálculo de insignias iniciado en segundo plano"
|
||||
msgid "Badge Type"
|
||||
msgstr "Tipo de insignia"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:66
|
||||
#: src/features/players/components/profile/index.tsx:68
|
||||
msgid "Badges"
|
||||
msgstr "Insignias"
|
||||
|
||||
@@ -645,7 +645,7 @@ msgid "Bracket"
|
||||
msgstr "Bracket"
|
||||
|
||||
#: src/app/routes/_authed/admin/preview.tsx:13
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:102
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:98
|
||||
msgid "Bracket Preview"
|
||||
msgstr "Vista previa del bracket"
|
||||
|
||||
@@ -720,7 +720,7 @@ msgstr "Elige un jugador"
|
||||
msgid "Claymation"
|
||||
msgstr "Plastilina"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:102
|
||||
#: src/features/players/components/league-head-to-head.tsx:107
|
||||
msgid "Clear selected player"
|
||||
msgstr "Quitar jugador seleccionado"
|
||||
|
||||
@@ -1118,7 +1118,7 @@ msgstr "Picos de errores y funciones fallando, enviados a esta cuenta"
|
||||
msgid "Error: {errorMessage}"
|
||||
msgstr "Error: {errorMessage}"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:57
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:63
|
||||
#: src/features/telemetry/components/usage-screen.tsx:260
|
||||
msgid "Errors"
|
||||
msgstr "Errores"
|
||||
@@ -1186,7 +1186,7 @@ msgstr "No se pudo establecer el dispositivo activo"
|
||||
msgid "Failed to set volume"
|
||||
msgstr "No se pudo ajustar el volumen"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:42
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:43
|
||||
msgid "Failed to start badge recalculation"
|
||||
msgstr "No se pudo iniciar el recálculo de insignias"
|
||||
|
||||
@@ -1198,8 +1198,8 @@ msgstr "No se pudo actualizar el equipo"
|
||||
msgid "Failed to update tournament"
|
||||
msgstr "No se pudo actualizar el torneo"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:141
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:139
|
||||
#: src/app/routes/api/teams/upload-logo.ts:142
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:140
|
||||
#: src/features/teams/components/team-form/index.tsx:148
|
||||
#: src/features/tournaments/components/tournament-form.tsx:88
|
||||
msgid "Failed to upload logo"
|
||||
@@ -1209,8 +1209,8 @@ msgstr "No se pudo subir el logo"
|
||||
msgid "Failure"
|
||||
msgstr "Fallo"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:80
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:81
|
||||
#: src/app/routes/api/teams/upload-logo.ts:81
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:82
|
||||
msgid "File too large. Maximum size is 10MB."
|
||||
msgstr "El archivo es demasiado grande. El tamaño máximo es 10MB."
|
||||
|
||||
@@ -1264,7 +1264,7 @@ msgstr "Función"
|
||||
msgid "Function Name"
|
||||
msgstr "Nombre de la función"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:51
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:57
|
||||
msgid "Functions"
|
||||
msgstr "Funciones"
|
||||
|
||||
@@ -1333,13 +1333,13 @@ msgstr "¡diviértete, mucha suerte!"
|
||||
msgid "Have you played before?"
|
||||
msgstr "¿Ya has jugado antes?"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:53
|
||||
#: src/app/routes/_authed/stats.tsx:56
|
||||
#: src/features/bracket/components/match-dock.tsx:174
|
||||
#: src/features/bracket/components/match-dock.tsx:219
|
||||
#: src/features/matches/components/match-card.tsx:86
|
||||
#: src/features/matches/components/match-card.tsx:230
|
||||
#: src/features/players/components/league-head-to-head.tsx:193
|
||||
#: src/features/players/components/profile/header.tsx:94
|
||||
#: src/features/bracket/components/match-dock.tsx:203
|
||||
#: src/features/matches/components/match-card.tsx:89
|
||||
#: src/features/matches/components/match-card.tsx:214
|
||||
#: src/features/players/components/league-head-to-head.tsx:198
|
||||
#: src/features/players/components/profile/header.tsx:79
|
||||
msgid "Head to Head"
|
||||
msgstr "Cara a cara"
|
||||
|
||||
@@ -1401,8 +1401,8 @@ msgstr "Código incorrecto"
|
||||
msgid "Invalid action"
|
||||
msgstr "Acción no válida"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:66
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:67
|
||||
#: src/app/routes/api/teams/upload-logo.ts:67
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:68
|
||||
msgid "Invalid file type. Only JPEG, PNG and GIF are allowed."
|
||||
msgstr "Tipo de archivo no válido. Solo se permiten JPEG, PNG y GIF."
|
||||
|
||||
@@ -1410,8 +1410,8 @@ msgstr "Tipo de archivo no válido. Solo se permiten JPEG, PNG y GIF."
|
||||
msgid "Invalid hex color format"
|
||||
msgstr "Formato de color hexadecimal no válido"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:34
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:35
|
||||
#: src/app/routes/api/teams/upload-logo.ts:35
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:36
|
||||
msgid "Invalid input"
|
||||
msgstr "Entrada no válida"
|
||||
|
||||
@@ -1470,7 +1470,7 @@ msgid "Last name must be 3-20 characters long and contain only letters"
|
||||
msgstr "El apellido debe tener entre 3 y 20 caracteres y contener solo letras"
|
||||
|
||||
#. placeholder {0}: migrationStatus.result.totalBadgesEarned
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:79
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:82
|
||||
msgid "Last recalculation: {0, plural, one {# badge earned} other {# badges earned}}"
|
||||
msgstr "Último recálculo: {0, plural, one {# insignia obtenida} other {# insignias obtenidas}}"
|
||||
|
||||
@@ -1504,8 +1504,8 @@ msgstr "La ubicación es obligatoria"
|
||||
msgid "Logo"
|
||||
msgstr "Logo"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:47
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:48
|
||||
#: src/app/routes/api/teams/upload-logo.ts:48
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:49
|
||||
msgid "Logo file is required"
|
||||
msgstr "El archivo del logo es obligatorio"
|
||||
|
||||
@@ -1530,8 +1530,8 @@ msgstr "Derrotas"
|
||||
msgid "M"
|
||||
msgstr "J"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:72
|
||||
#: src/features/players/components/profile/index.tsx:36
|
||||
#: src/app/routes/_authed/stats.tsx:75
|
||||
#: src/features/players/components/profile/index.tsx:38
|
||||
msgid "Mainline"
|
||||
msgstr "Mainline"
|
||||
|
||||
@@ -1559,7 +1559,7 @@ msgid "Manage Team Songs/Logos"
|
||||
msgstr "Administrar canciones/logos del equipo"
|
||||
|
||||
#: src/app/routes/_authed/admin/tournaments/index.tsx:17
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:50
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:51
|
||||
msgid "Manage Tournaments"
|
||||
msgstr "Administrar torneos"
|
||||
|
||||
@@ -1611,8 +1611,8 @@ msgstr "Historial de partidos ({totalMatches})"
|
||||
msgid "Match started!"
|
||||
msgstr "¡Empezó el partido!"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:376
|
||||
#: src/features/players/components/profile/index.tsx:85
|
||||
#: src/features/players/components/player-stats-table.tsx:375
|
||||
#: src/features/players/components/profile/index.tsx:87
|
||||
#: src/features/players/components/profile/skeleton.tsx:70
|
||||
#: src/features/teams/components/team-profile/index.tsx:44
|
||||
#: src/features/teams/components/team-profile/skeleton.tsx:75
|
||||
@@ -1621,7 +1621,7 @@ msgstr "¡Empezó el partido!"
|
||||
msgid "Matches"
|
||||
msgstr "Partidos"
|
||||
|
||||
#: src/features/matches/components/match-list.tsx:33
|
||||
#: src/features/matches/components/match-list.tsx:34
|
||||
msgid "Matches for regionals are unordered"
|
||||
msgstr "Los partidos de regionales no tienen un orden específico"
|
||||
|
||||
@@ -1630,11 +1630,11 @@ msgstr "Los partidos de regionales no tienen un orden específico"
|
||||
msgid "Matches Played"
|
||||
msgstr "Partidos jugados"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:249
|
||||
#: src/features/predictions/components/prediction-editor.tsx:253
|
||||
msgid "Matchup"
|
||||
msgstr "Enfrentamiento"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:226
|
||||
#: src/features/predictions/components/prediction-editor.tsx:230
|
||||
msgid "Matchup details"
|
||||
msgstr "Detalles del enfrentamiento"
|
||||
|
||||
@@ -1642,20 +1642,20 @@ msgstr "Detalles del enfrentamiento"
|
||||
msgid "MAU"
|
||||
msgstr "MAU"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:123
|
||||
#: src/features/players/components/league-head-to-head.tsx:128
|
||||
msgid "Me"
|
||||
msgstr "Yo"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:65
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:68
|
||||
msgid "Migrate Badge Progress"
|
||||
msgstr "Migrar progreso de insignias"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:131
|
||||
#: src/features/players/components/player-stats-table.tsx:356
|
||||
#: src/features/players/components/player-stats-table.tsx:355
|
||||
msgid "MMR"
|
||||
msgstr "MMR"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:431
|
||||
#: src/features/players/components/player-stats-table.tsx:430
|
||||
msgid "MMR Calculation:"
|
||||
msgstr "Cálculo de MMR:"
|
||||
|
||||
@@ -1712,7 +1712,7 @@ msgstr "Nuevo error de cliente"
|
||||
msgid "New Player"
|
||||
msgstr "Nuevo jugador"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:211
|
||||
#: src/features/predictions/components/prediction-editor.tsx:212
|
||||
msgid "Next match"
|
||||
msgstr "Siguiente partido"
|
||||
|
||||
@@ -1775,7 +1775,7 @@ msgstr "No hay partidos programados"
|
||||
msgid "No player activity found"
|
||||
msgstr "No se encontró actividad de jugadores"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:151
|
||||
#: src/features/players/components/league-head-to-head.tsx:156
|
||||
msgid "No players available"
|
||||
msgstr "No hay jugadores disponibles"
|
||||
|
||||
@@ -1784,8 +1784,8 @@ msgstr "No hay jugadores disponibles"
|
||||
msgid "No players enrolled yet"
|
||||
msgstr "Aún no hay jugadores inscritos"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:151
|
||||
#: src/features/players/components/player-stats-table.tsx:486
|
||||
#: src/features/players/components/league-head-to-head.tsx:156
|
||||
#: src/features/players/components/player-stats-table.tsx:485
|
||||
msgid "No players found matching \"{search}\""
|
||||
msgstr "No se encontraron jugadores que coincidan con \"{search}\""
|
||||
|
||||
@@ -1818,7 +1818,7 @@ msgstr "No hay canción seleccionada"
|
||||
msgid "No state captured"
|
||||
msgstr "No se capturó ningún estado"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:319
|
||||
#: src/features/players/components/player-stats-table.tsx:318
|
||||
msgid "No Stats Available"
|
||||
msgstr "No hay estadísticas disponibles"
|
||||
|
||||
@@ -1843,7 +1843,7 @@ msgstr "Aún no hay equipos inscritos"
|
||||
msgid "No teams found matching \"{search}\""
|
||||
msgstr "No se encontraron equipos que coincidan con \"{search}\""
|
||||
|
||||
#: src/features/tournaments/components/tournament-list.tsx:95
|
||||
#: src/features/tournaments/components/tournament-list.tsx:98
|
||||
msgid "No tournaments found"
|
||||
msgstr "No se encontraron torneos"
|
||||
|
||||
@@ -1908,10 +1908,6 @@ msgstr "Una vez inscrito, puedes ver a otros agentes libres y su número de tel
|
||||
msgid "One time code"
|
||||
msgstr "Código de un solo uso"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:95
|
||||
msgid "Open Pocketbase"
|
||||
msgstr "Abrir Pocketbase"
|
||||
|
||||
#: src/features/bracket/components/match-report.tsx:273
|
||||
msgid "Opponent"
|
||||
msgstr "Rival"
|
||||
@@ -1937,7 +1933,7 @@ msgstr "Las predicciones de otros jugadores están ocultas hasta que empiece el
|
||||
msgid "Overtime count is required"
|
||||
msgstr "La cantidad de tiempos extra es obligatoria"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:61
|
||||
#: src/features/players/components/profile/index.tsx:63
|
||||
#: src/features/players/components/profile/skeleton.tsx:65
|
||||
#: src/features/teams/components/team-profile/index.tsx:25
|
||||
#: src/features/teams/components/team-profile/skeleton.tsx:70
|
||||
@@ -2047,7 +2043,7 @@ msgstr "Jugador eliminado con éxito"
|
||||
msgid "Player Stats"
|
||||
msgstr "Estadísticas de jugador"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:48
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
#: src/features/teams/components/team-profile/index.tsx:29
|
||||
msgid "Players"
|
||||
msgstr "Jugadores"
|
||||
@@ -2083,7 +2079,7 @@ msgstr "Predicciones"
|
||||
msgid "Predictions are open"
|
||||
msgstr "Las predicciones están abiertas"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:201
|
||||
#: src/features/predictions/components/prediction-editor.tsx:202
|
||||
msgid "Previous match"
|
||||
msgstr "Partido anterior"
|
||||
|
||||
@@ -2095,7 +2091,7 @@ msgstr "Pista anterior"
|
||||
msgid "Profile"
|
||||
msgstr "Perfil"
|
||||
|
||||
#: src/features/badges/components/badge-showcase.tsx:301
|
||||
#: src/features/badges/components/badge-showcase.tsx:302
|
||||
msgid "Progress "
|
||||
msgstr "Progreso "
|
||||
|
||||
@@ -2116,13 +2112,13 @@ msgstr "Misiones"
|
||||
msgid "Re-roll"
|
||||
msgstr "Volver a sortear"
|
||||
|
||||
#: src/features/reactions/components/emoji-bar.tsx:213
|
||||
#: src/features/reactions/components/emoji-bar.tsx:210
|
||||
msgid "Reactions"
|
||||
msgstr "Reacciones"
|
||||
|
||||
#. placeholder {0}: migrationStatus.playersProcessed
|
||||
#. placeholder {1}: migrationStatus.totalPlayers
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:72
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:75
|
||||
msgid "Recalculating badges in the background ({0}/{1})"
|
||||
msgstr "Recalculando insignias en segundo plano ({0}/{1})"
|
||||
|
||||
@@ -2130,8 +2126,8 @@ msgstr "Recalculando insignias en segundo plano ({0}/{1})"
|
||||
msgid "Recently finished"
|
||||
msgstr "Recién terminados"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:79
|
||||
#: src/features/players/components/profile/index.tsx:37
|
||||
#: src/app/routes/_authed/stats.tsx:82
|
||||
#: src/features/players/components/profile/index.tsx:39
|
||||
msgid "Regional"
|
||||
msgstr "Regional"
|
||||
|
||||
@@ -2182,7 +2178,7 @@ msgid "rofl"
|
||||
msgstr "carcajada"
|
||||
|
||||
#. placeholder {0}: match.round + 1
|
||||
#: src/features/matches/components/match-card.tsx:79
|
||||
#: src/features/matches/components/match-card.tsx:82
|
||||
msgid "Round {0}"
|
||||
msgstr "Ronda {0}"
|
||||
|
||||
@@ -2245,8 +2241,8 @@ msgstr "Buscar canciones..."
|
||||
msgid "Search for teams to enroll..."
|
||||
msgstr "Buscar equipos para inscribir..."
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:132
|
||||
#: src/features/players/components/player-stats-table.tsx:332
|
||||
#: src/features/players/components/league-head-to-head.tsx:137
|
||||
#: src/features/players/components/player-stats-table.tsx:331
|
||||
msgid "Search players"
|
||||
msgstr "Buscar jugadores"
|
||||
|
||||
@@ -2283,7 +2279,7 @@ msgstr "Selecciona una insignia"
|
||||
msgid "Select a device..."
|
||||
msgstr "Selecciona un dispositivo..."
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:112
|
||||
#: src/features/players/components/league-head-to-head.tsx:117
|
||||
msgid "Select a player"
|
||||
msgstr "Selecciona un jugador"
|
||||
|
||||
@@ -2366,7 +2362,7 @@ msgstr "Funciones del servidor"
|
||||
msgid "serverFn name"
|
||||
msgstr "nombre de serverFn"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:60
|
||||
#: src/features/telemetry/components/usage-screen.tsx:212
|
||||
msgid "Sessions"
|
||||
msgstr "Sesiones"
|
||||
@@ -2382,7 +2378,7 @@ msgstr "Mostrar detalles"
|
||||
|
||||
#. placeholder {0}: filteredAndSortedStats.length
|
||||
#. placeholder {1}: playerStats.length
|
||||
#: src/features/players/components/player-stats-table.tsx:329
|
||||
#: src/features/players/components/player-stats-table.tsx:328
|
||||
msgid "Showing {0} of {1} players"
|
||||
msgstr "Mostrando {0} de {1} jugadores"
|
||||
|
||||
@@ -2434,7 +2430,7 @@ msgstr "El segmento de la canción no debe durar más de 15 segundos"
|
||||
msgid "Song start time is required"
|
||||
msgstr "Se requiere la hora de inicio de la canción"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:350
|
||||
#: src/features/players/components/player-stats-table.tsx:349
|
||||
#: src/features/telemetry/components/shared/search-filter-bar.tsx:102
|
||||
msgid "Sort:"
|
||||
msgstr "Ordenar:"
|
||||
@@ -2489,21 +2485,21 @@ msgstr "Empieza a escribir para buscar..."
|
||||
msgid "Starts in"
|
||||
msgstr "Comienza en"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:382
|
||||
#: src/features/players/components/player-stats-table.tsx:381
|
||||
msgid "Stat abbreviations and MMR info"
|
||||
msgstr "Abreviaturas de estadísticas e información de MMR"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:389
|
||||
#: src/features/players/components/player-stats-table.tsx:388
|
||||
msgid "Stat Abbreviations:"
|
||||
msgstr "Abreviaturas de estadísticas:"
|
||||
|
||||
#: src/features/core/hooks/use-links.ts:17
|
||||
#: src/features/players/components/profile/index.tsx:33
|
||||
#: src/features/players/components/profile/index.tsx:35
|
||||
#: src/features/teams/components/team-profile/index.tsx:34
|
||||
msgid "Statistics"
|
||||
msgstr "Estadísticas"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:52
|
||||
#: src/app/routes/_authed/stats.tsx:55
|
||||
msgid "Stats"
|
||||
msgstr "Estadísticas"
|
||||
|
||||
@@ -2520,7 +2516,7 @@ msgstr "Estado"
|
||||
#: src/components/sheet/slide-panel/slide-panel.tsx:40
|
||||
#: src/features/login/components/player-prompt/index.tsx:115
|
||||
#: src/features/login/components/player-prompt/index.tsx:126
|
||||
#: src/features/predictions/components/prediction-editor.tsx:240
|
||||
#: src/features/predictions/components/prediction-editor.tsx:244
|
||||
#: src/features/tournaments/components/edit-rules.tsx:39
|
||||
msgid "Submit"
|
||||
msgstr "Enviar"
|
||||
@@ -2543,7 +2539,7 @@ msgstr "Sistema"
|
||||
msgid "T"
|
||||
msgstr "T"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:92
|
||||
#: src/features/players/components/league-head-to-head.tsx:97
|
||||
msgid "Tap an opponent below to compare"
|
||||
msgstr "Toca un oponente abajo para comparar"
|
||||
|
||||
@@ -2581,7 +2577,7 @@ msgstr "Detalles del equipo"
|
||||
msgid "Team enrolled successfully!"
|
||||
msgstr "¡Equipo inscrito exitosamente!"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:93
|
||||
#: src/app/routes/api/teams/upload-logo.ts:94
|
||||
#: src/features/teams/components/team-profile/index.tsx:21
|
||||
msgid "Team not found"
|
||||
msgstr "Equipo no encontrado"
|
||||
@@ -2603,7 +2599,7 @@ msgstr "Equipo actualizado, pero falló la carga del logo: {0}"
|
||||
msgid "Team updated successfully!"
|
||||
msgstr "¡Equipo actualizado exitosamente!"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:90
|
||||
#: src/features/players/components/profile/index.tsx:92
|
||||
#: src/features/players/components/profile/skeleton.tsx:75
|
||||
#: src/features/tournaments/components/profile/index.tsx:41
|
||||
#: src/features/tournaments/components/profile/skeleton.tsx:89
|
||||
@@ -2615,8 +2611,8 @@ msgstr "Equipos"
|
||||
msgid "Teams created successfully!"
|
||||
msgstr "¡Equipos creados exitosamente!"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:20
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:55
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:26
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:57
|
||||
msgid "Telemetry"
|
||||
msgstr "Telemetría"
|
||||
|
||||
@@ -2730,7 +2726,7 @@ msgstr "Torneo creado, pero falló la carga del logo: {0}"
|
||||
msgid "Tournament created successfully!"
|
||||
msgstr "¡Torneo creado exitosamente!"
|
||||
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:94
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:95
|
||||
msgid "Tournament not found"
|
||||
msgstr "Torneo no encontrado"
|
||||
|
||||
@@ -2784,8 +2780,8 @@ msgstr "Cancelar inscripción del equipo"
|
||||
msgid "Unknown error"
|
||||
msgstr "Error desconocido"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:142
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:140
|
||||
#: src/app/routes/api/teams/upload-logo.ts:143
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:141
|
||||
msgid "Unknown error occurred"
|
||||
msgstr "Ocurrió un error desconocido"
|
||||
|
||||
@@ -2810,7 +2806,7 @@ msgstr "Próximos"
|
||||
msgid "Update Match"
|
||||
msgstr "Actualizar partido"
|
||||
|
||||
#: src/features/players/components/profile/header.tsx:89
|
||||
#: src/features/players/components/profile/header.tsx:74
|
||||
msgid "Update Name"
|
||||
msgstr "Actualizar nombre"
|
||||
|
||||
@@ -2824,7 +2820,7 @@ msgstr "Actualizar equipo"
|
||||
msgid "Update Tournament"
|
||||
msgstr "Actualizar torneo"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:45
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:51
|
||||
msgid "Usage"
|
||||
msgstr "Uso"
|
||||
|
||||
@@ -2837,7 +2833,7 @@ msgstr "User Agent"
|
||||
msgid "versus"
|
||||
msgstr "contra"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:73
|
||||
#: src/features/players/components/profile/index.tsx:75
|
||||
msgid "View all badges"
|
||||
msgstr "Ver todas las insignias"
|
||||
|
||||
@@ -2853,8 +2849,8 @@ msgstr "Ver bracket"
|
||||
msgid "View Groups"
|
||||
msgstr "Ver grupos"
|
||||
|
||||
#: src/features/bracket/components/match-dock.tsx:182
|
||||
#: src/features/matches/components/match-card.tsx:91
|
||||
#: src/features/bracket/components/match-dock.tsx:187
|
||||
#: src/features/matches/components/match-card.tsx:94
|
||||
msgid "View head-to-head"
|
||||
msgstr "Ver cara a cara"
|
||||
|
||||
@@ -2935,7 +2931,7 @@ msgstr "Final del bracket de ganadores"
|
||||
|
||||
#: src/components/stats-overview.tsx:103
|
||||
#: src/components/stats-overview.tsx:136
|
||||
#: src/features/players/components/player-stats-table.tsx:366
|
||||
#: src/features/players/components/player-stats-table.tsx:365
|
||||
msgid "Wins"
|
||||
msgstr "Victorias"
|
||||
|
||||
|
||||
+101
-105
@@ -13,7 +13,7 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/features/matches/components/match-card.tsx:80
|
||||
#: src/features/matches/components/match-card.tsx:83
|
||||
msgid " (Losers)"
|
||||
msgstr " (敗者側)"
|
||||
|
||||
@@ -93,7 +93,7 @@ msgstr "{0, plural, one {人のプレイヤーが登録済み} other {人のプ
|
||||
|
||||
#. placeholder {0}: tournamentStart.toLocaleDateString(i18n.locale, { weekday: "short", month: "short", day: "numeric", })
|
||||
#. placeholder {1}: tournamentStart.toLocaleTimeString(i18n.locale, { hour: "2-digit", minute: "2-digit", })
|
||||
#: src/features/tournaments/components/started-tournament/header.tsx:51
|
||||
#: src/features/tournaments/components/started-tournament/header.tsx:56
|
||||
msgid "{0} at {1}"
|
||||
msgstr "{0} {1}"
|
||||
|
||||
@@ -228,7 +228,7 @@ msgstr "{namesList}他1名"
|
||||
msgid "{size}-team knockout"
|
||||
msgstr "{size}チームノックアウト"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:446
|
||||
#: src/features/players/components/player-stats-table.tsx:445
|
||||
msgid "* Confidence penalty applied for players with <15 matches"
|
||||
msgstr "※試合数が15未満のプレイヤーには信頼度ペナルティが適用されます"
|
||||
|
||||
@@ -241,7 +241,7 @@ msgstr "※必要な場合"
|
||||
msgid "* PICKS shows correct picks / total picks made"
|
||||
msgstr "※PICKSは正解数/予想総数を表示"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:449
|
||||
#: src/features/players/components/player-stats-table.tsx:448
|
||||
msgid "** Not an official rating"
|
||||
msgstr "※※公式レーティングではありません"
|
||||
|
||||
@@ -254,19 +254,19 @@ msgstr "&"
|
||||
msgid "#{0}"
|
||||
msgstr "#{0}"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:416
|
||||
#: src/features/players/components/player-stats-table.tsx:415
|
||||
msgid "• <0>AC:</0> Average Cups Per Match"
|
||||
msgstr "• <0>AC:</0>平均獲得カップ数(1試合あたり)"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:419
|
||||
#: src/features/players/components/player-stats-table.tsx:418
|
||||
msgid "• <0>ACA:</0> Average Cups Against"
|
||||
msgstr "• <0>ACA:</0>平均失カップ数"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:413
|
||||
#: src/features/players/components/player-stats-table.tsx:412
|
||||
msgid "• <0>ALM:</0> Average Loss Margin"
|
||||
msgstr "• <0>ALM:</0>平均敗差"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:410
|
||||
#: src/features/players/components/player-stats-table.tsx:409
|
||||
msgid "• <0>AWM:</0> Average Win Margin"
|
||||
msgstr "• <0>AWM:</0>平均勝差"
|
||||
|
||||
@@ -274,15 +274,15 @@ msgstr "• <0>AWM:</0>平均勝差"
|
||||
msgid "• <0>Bracket reset:</0> only picked if your bracket triggers it — worth double the Final"
|
||||
msgstr "• <0>ブラケットリセット:</0>自分のブラケットで発生した場合のみ選択可能 — ファイナルの2倍の得点"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:425
|
||||
#: src/features/players/components/player-stats-table.tsx:424
|
||||
msgid "• <0>CA:</0> Cups Against"
|
||||
msgstr "• <0>CA:</0>失カップ数"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:422
|
||||
#: src/features/players/components/player-stats-table.tsx:421
|
||||
msgid "• <0>CF:</0> Cups For"
|
||||
msgstr "• <0>CF:</0>獲得カップ数"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:404
|
||||
#: src/features/players/components/player-stats-table.tsx:403
|
||||
msgid "• <0>L:</0> Losses"
|
||||
msgstr "• <0>L:</0>敗数"
|
||||
|
||||
@@ -290,23 +290,23 @@ msgstr "• <0>L:</0>敗数"
|
||||
msgid "• <0>Losers bracket:</0> 5, 10, 20, 40…"
|
||||
msgstr "• <0>敗者ブラケット:</0>5、10、20、40…"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:392
|
||||
#: src/features/players/components/player-stats-table.tsx:391
|
||||
msgid "• <0>M:</0> Matches"
|
||||
msgstr "• <0>M:</0>試合数"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:398
|
||||
#: src/features/players/components/player-stats-table.tsx:397
|
||||
msgid "• <0>MMR:</0> Matchmaking Rating"
|
||||
msgstr "• <0>MMR:</0>マッチメイキングレーティング"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:395
|
||||
#: src/features/players/components/player-stats-table.tsx:394
|
||||
msgid "• <0>T:</0> Tournaments"
|
||||
msgstr "• <0>T:</0>トーナメント数"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:401
|
||||
#: src/features/players/components/player-stats-table.tsx:400
|
||||
msgid "• <0>W:</0> Wins"
|
||||
msgstr "• <0>W:</0>勝数"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:407
|
||||
#: src/features/players/components/player-stats-table.tsx:406
|
||||
msgid "• <0>W%:</0> Win Percentage"
|
||||
msgstr "• <0>W%:</0>勝率"
|
||||
|
||||
@@ -314,11 +314,11 @@ msgstr "• <0>W%:</0>勝率"
|
||||
msgid "• <0>Winners bracket:</0> 10, 20, 40, 80…"
|
||||
msgstr "• <0>勝者ブラケット:</0>10、20、40、80…"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:437
|
||||
#: src/features/players/components/player-stats-table.tsx:436
|
||||
msgid "• Average Cups/Match (25%)"
|
||||
msgstr "• 平均獲得カップ数/試合(25%)"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:440
|
||||
#: src/features/players/components/player-stats-table.tsx:439
|
||||
msgid "• Average Win Margin (15%)"
|
||||
msgstr "• 平均勝差(15%)"
|
||||
|
||||
@@ -326,11 +326,11 @@ msgstr "• 平均勝差(15%)"
|
||||
msgid "• Each correct pick earns points, doubling every round"
|
||||
msgstr "• 予想が的中するたびにポイント獲得、ラウンドごとに倍増"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:443
|
||||
#: src/features/players/components/player-stats-table.tsx:442
|
||||
msgid "• Match Volume Bonus (10%)"
|
||||
msgstr "• 試合数ボーナス(10%)"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:434
|
||||
#: src/features/players/components/player-stats-table.tsx:433
|
||||
msgid "• Win Rate (50%)"
|
||||
msgstr "• 勝率(50%)"
|
||||
|
||||
@@ -457,9 +457,9 @@ msgstr "管理者"
|
||||
msgid "Aero"
|
||||
msgstr "エアロ"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:65
|
||||
#: src/app/routes/_authed/stats.tsx:68
|
||||
#: src/features/activities/components/activities-table.tsx:92
|
||||
#: src/features/players/components/profile/index.tsx:35
|
||||
#: src/features/players/components/profile/index.tsx:37
|
||||
msgid "All"
|
||||
msgstr "すべて"
|
||||
|
||||
@@ -572,7 +572,7 @@ msgstr "平均勝差"
|
||||
msgid "Award Badge"
|
||||
msgstr "バッジを付与"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:60
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:63
|
||||
msgid "Award Badges"
|
||||
msgstr "バッジを付与"
|
||||
|
||||
@@ -606,15 +606,15 @@ msgstr "戻る"
|
||||
msgid "Badge awarded to {playerName}"
|
||||
msgstr "{playerName}にバッジを付与しました"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:91
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:94
|
||||
msgid "Badge recalculation failed. Check the server logs."
|
||||
msgstr "バッジの再計算に失敗しました。サーバーログを確認してください。"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:39
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:40
|
||||
msgid "Badge recalculation is already running"
|
||||
msgstr "バッジの再計算はすでに実行中です"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:38
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:39
|
||||
msgid "Badge recalculation started in the background"
|
||||
msgstr "バックグラウンドでバッジの再計算を開始しました"
|
||||
|
||||
@@ -622,7 +622,7 @@ msgstr "バックグラウンドでバッジの再計算を開始しました"
|
||||
msgid "Badge Type"
|
||||
msgstr "バッジの種類"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:66
|
||||
#: src/features/players/components/profile/index.tsx:68
|
||||
msgid "Badges"
|
||||
msgstr "バッジ"
|
||||
|
||||
@@ -645,7 +645,7 @@ msgid "Bracket"
|
||||
msgstr "ブラケット"
|
||||
|
||||
#: src/app/routes/_authed/admin/preview.tsx:13
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:102
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:98
|
||||
msgid "Bracket Preview"
|
||||
msgstr "ブラケットプレビュー"
|
||||
|
||||
@@ -720,7 +720,7 @@ msgstr "プレイヤーを選択"
|
||||
msgid "Claymation"
|
||||
msgstr "クレイ"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:102
|
||||
#: src/features/players/components/league-head-to-head.tsx:107
|
||||
msgid "Clear selected player"
|
||||
msgstr "選択したプレイヤーを解除"
|
||||
|
||||
@@ -1118,7 +1118,7 @@ msgstr "エラーの急増や失敗している関数をこのアカウントに
|
||||
msgid "Error: {errorMessage}"
|
||||
msgstr "エラー: {errorMessage}"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:57
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:63
|
||||
#: src/features/telemetry/components/usage-screen.tsx:260
|
||||
msgid "Errors"
|
||||
msgstr "エラー"
|
||||
@@ -1186,7 +1186,7 @@ msgstr "アクティブなデバイスの設定に失敗しました"
|
||||
msgid "Failed to set volume"
|
||||
msgstr "音量の設定に失敗しました"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:42
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:43
|
||||
msgid "Failed to start badge recalculation"
|
||||
msgstr "バッジの再計算を開始できませんでした"
|
||||
|
||||
@@ -1198,8 +1198,8 @@ msgstr "チームの更新に失敗しました"
|
||||
msgid "Failed to update tournament"
|
||||
msgstr "トーナメントの更新に失敗しました"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:141
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:139
|
||||
#: src/app/routes/api/teams/upload-logo.ts:142
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:140
|
||||
#: src/features/teams/components/team-form/index.tsx:148
|
||||
#: src/features/tournaments/components/tournament-form.tsx:88
|
||||
msgid "Failed to upload logo"
|
||||
@@ -1209,8 +1209,8 @@ msgstr "ロゴのアップロードに失敗しました"
|
||||
msgid "Failure"
|
||||
msgstr "失敗"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:80
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:81
|
||||
#: src/app/routes/api/teams/upload-logo.ts:81
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:82
|
||||
msgid "File too large. Maximum size is 10MB."
|
||||
msgstr "ファイルサイズが大きすぎます。最大サイズは10MBです。"
|
||||
|
||||
@@ -1264,7 +1264,7 @@ msgstr "関数"
|
||||
msgid "Function Name"
|
||||
msgstr "関数名"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:51
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:57
|
||||
msgid "Functions"
|
||||
msgstr "関数"
|
||||
|
||||
@@ -1333,13 +1333,13 @@ msgstr "楽しんで、頑張ってください!"
|
||||
msgid "Have you played before?"
|
||||
msgstr "参加したことはありますか?"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:53
|
||||
#: src/app/routes/_authed/stats.tsx:56
|
||||
#: src/features/bracket/components/match-dock.tsx:174
|
||||
#: src/features/bracket/components/match-dock.tsx:219
|
||||
#: src/features/matches/components/match-card.tsx:86
|
||||
#: src/features/matches/components/match-card.tsx:230
|
||||
#: src/features/players/components/league-head-to-head.tsx:193
|
||||
#: src/features/players/components/profile/header.tsx:94
|
||||
#: src/features/bracket/components/match-dock.tsx:203
|
||||
#: src/features/matches/components/match-card.tsx:89
|
||||
#: src/features/matches/components/match-card.tsx:214
|
||||
#: src/features/players/components/league-head-to-head.tsx:198
|
||||
#: src/features/players/components/profile/header.tsx:79
|
||||
msgid "Head to Head"
|
||||
msgstr "対戦成績"
|
||||
|
||||
@@ -1401,8 +1401,8 @@ msgstr "コードが正しくありません"
|
||||
msgid "Invalid action"
|
||||
msgstr "無効な操作です"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:66
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:67
|
||||
#: src/app/routes/api/teams/upload-logo.ts:67
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:68
|
||||
msgid "Invalid file type. Only JPEG, PNG and GIF are allowed."
|
||||
msgstr "無効なファイル形式です。JPEG、PNG、GIFのみ使用できます。"
|
||||
|
||||
@@ -1410,8 +1410,8 @@ msgstr "無効なファイル形式です。JPEG、PNG、GIFのみ使用でき
|
||||
msgid "Invalid hex color format"
|
||||
msgstr "無効なHEXカラー形式です"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:34
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:35
|
||||
#: src/app/routes/api/teams/upload-logo.ts:35
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:36
|
||||
msgid "Invalid input"
|
||||
msgstr "無効な入力です"
|
||||
|
||||
@@ -1470,7 +1470,7 @@ msgid "Last name must be 3-20 characters long and contain only letters"
|
||||
msgstr "姓は3〜20文字の英字のみで入力してください"
|
||||
|
||||
#. placeholder {0}: migrationStatus.result.totalBadgesEarned
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:79
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:82
|
||||
msgid "Last recalculation: {0, plural, one {# badge earned} other {# badges earned}}"
|
||||
msgstr "前回の再計算:{0, plural, one {# 個のバッジを獲得} other {# 個のバッジを獲得}}"
|
||||
|
||||
@@ -1504,8 +1504,8 @@ msgstr "場所は必須です"
|
||||
msgid "Logo"
|
||||
msgstr "ロゴ"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:47
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:48
|
||||
#: src/app/routes/api/teams/upload-logo.ts:48
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:49
|
||||
msgid "Logo file is required"
|
||||
msgstr "ロゴファイルは必須です"
|
||||
|
||||
@@ -1530,8 +1530,8 @@ msgstr "敗数"
|
||||
msgid "M"
|
||||
msgstr "M"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:72
|
||||
#: src/features/players/components/profile/index.tsx:36
|
||||
#: src/app/routes/_authed/stats.tsx:75
|
||||
#: src/features/players/components/profile/index.tsx:38
|
||||
msgid "Mainline"
|
||||
msgstr "メインライン"
|
||||
|
||||
@@ -1559,7 +1559,7 @@ msgid "Manage Team Songs/Logos"
|
||||
msgstr "チームの曲・ロゴを管理"
|
||||
|
||||
#: src/app/routes/_authed/admin/tournaments/index.tsx:17
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:50
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:51
|
||||
msgid "Manage Tournaments"
|
||||
msgstr "トーナメント管理"
|
||||
|
||||
@@ -1611,8 +1611,8 @@ msgstr "対戦履歴({totalMatches})"
|
||||
msgid "Match started!"
|
||||
msgstr "試合開始!"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:376
|
||||
#: src/features/players/components/profile/index.tsx:85
|
||||
#: src/features/players/components/player-stats-table.tsx:375
|
||||
#: src/features/players/components/profile/index.tsx:87
|
||||
#: src/features/players/components/profile/skeleton.tsx:70
|
||||
#: src/features/teams/components/team-profile/index.tsx:44
|
||||
#: src/features/teams/components/team-profile/skeleton.tsx:75
|
||||
@@ -1621,7 +1621,7 @@ msgstr "試合開始!"
|
||||
msgid "Matches"
|
||||
msgstr "試合"
|
||||
|
||||
#: src/features/matches/components/match-list.tsx:33
|
||||
#: src/features/matches/components/match-list.tsx:34
|
||||
msgid "Matches for regionals are unordered"
|
||||
msgstr "地区予選の試合は順不同です"
|
||||
|
||||
@@ -1630,11 +1630,11 @@ msgstr "地区予選の試合は順不同です"
|
||||
msgid "Matches Played"
|
||||
msgstr "試合数"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:249
|
||||
#: src/features/predictions/components/prediction-editor.tsx:253
|
||||
msgid "Matchup"
|
||||
msgstr "対戦カード"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:226
|
||||
#: src/features/predictions/components/prediction-editor.tsx:230
|
||||
msgid "Matchup details"
|
||||
msgstr "対戦カードの詳細"
|
||||
|
||||
@@ -1642,20 +1642,20 @@ msgstr "対戦カードの詳細"
|
||||
msgid "MAU"
|
||||
msgstr "MAU"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:123
|
||||
#: src/features/players/components/league-head-to-head.tsx:128
|
||||
msgid "Me"
|
||||
msgstr "自分"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:65
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:68
|
||||
msgid "Migrate Badge Progress"
|
||||
msgstr "バッジ進捗を移行"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:131
|
||||
#: src/features/players/components/player-stats-table.tsx:356
|
||||
#: src/features/players/components/player-stats-table.tsx:355
|
||||
msgid "MMR"
|
||||
msgstr "MMR"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:431
|
||||
#: src/features/players/components/player-stats-table.tsx:430
|
||||
msgid "MMR Calculation:"
|
||||
msgstr "MMR計算:"
|
||||
|
||||
@@ -1712,7 +1712,7 @@ msgstr "新しいクライアントエラー"
|
||||
msgid "New Player"
|
||||
msgstr "新しいプレイヤー"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:211
|
||||
#: src/features/predictions/components/prediction-editor.tsx:212
|
||||
msgid "Next match"
|
||||
msgstr "次の試合"
|
||||
|
||||
@@ -1775,7 +1775,7 @@ msgstr "予定されている試合はありません"
|
||||
msgid "No player activity found"
|
||||
msgstr "プレイヤーのアクティビティが見つかりません"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:151
|
||||
#: src/features/players/components/league-head-to-head.tsx:156
|
||||
msgid "No players available"
|
||||
msgstr "利用可能なプレイヤーがいません"
|
||||
|
||||
@@ -1784,8 +1784,8 @@ msgstr "利用可能なプレイヤーがいません"
|
||||
msgid "No players enrolled yet"
|
||||
msgstr "まだプレイヤーが登録されていません"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:151
|
||||
#: src/features/players/components/player-stats-table.tsx:486
|
||||
#: src/features/players/components/league-head-to-head.tsx:156
|
||||
#: src/features/players/components/player-stats-table.tsx:485
|
||||
msgid "No players found matching \"{search}\""
|
||||
msgstr "「{search}」に一致するプレイヤーが見つかりません"
|
||||
|
||||
@@ -1818,7 +1818,7 @@ msgstr "曲が選択されていません"
|
||||
msgid "No state captured"
|
||||
msgstr "キャプチャされた状態がありません"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:319
|
||||
#: src/features/players/components/player-stats-table.tsx:318
|
||||
msgid "No Stats Available"
|
||||
msgstr "利用可能な統計がありません"
|
||||
|
||||
@@ -1843,7 +1843,7 @@ msgstr "まだチームが登録されていません"
|
||||
msgid "No teams found matching \"{search}\""
|
||||
msgstr "「{search}」に一致するチームが見つかりません"
|
||||
|
||||
#: src/features/tournaments/components/tournament-list.tsx:95
|
||||
#: src/features/tournaments/components/tournament-list.tsx:98
|
||||
msgid "No tournaments found"
|
||||
msgstr "トーナメントが見つかりません"
|
||||
|
||||
@@ -1908,10 +1908,6 @@ msgstr "登録が完了すると、他のフリーエージェントとその電
|
||||
msgid "One time code"
|
||||
msgstr "ワンタイムコード"
|
||||
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:95
|
||||
msgid "Open Pocketbase"
|
||||
msgstr "Pocketbaseを開く"
|
||||
|
||||
#: src/features/bracket/components/match-report.tsx:273
|
||||
msgid "Opponent"
|
||||
msgstr "対戦相手"
|
||||
@@ -1937,7 +1933,7 @@ msgstr "トーナメントが始まるまで、他のプレイヤーの予想は
|
||||
msgid "Overtime count is required"
|
||||
msgstr "OT回数の入力が必要です"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:61
|
||||
#: src/features/players/components/profile/index.tsx:63
|
||||
#: src/features/players/components/profile/skeleton.tsx:65
|
||||
#: src/features/teams/components/team-profile/index.tsx:25
|
||||
#: src/features/teams/components/team-profile/skeleton.tsx:70
|
||||
@@ -2047,7 +2043,7 @@ msgstr "プレイヤーを削除しました"
|
||||
msgid "Player Stats"
|
||||
msgstr "プレイヤースタッツ"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:48
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
#: src/features/teams/components/team-profile/index.tsx:29
|
||||
msgid "Players"
|
||||
msgstr "プレイヤー"
|
||||
@@ -2083,7 +2079,7 @@ msgstr "予想"
|
||||
msgid "Predictions are open"
|
||||
msgstr "予想受付中"
|
||||
|
||||
#: src/features/predictions/components/prediction-editor.tsx:201
|
||||
#: src/features/predictions/components/prediction-editor.tsx:202
|
||||
msgid "Previous match"
|
||||
msgstr "前の試合"
|
||||
|
||||
@@ -2095,7 +2091,7 @@ msgstr "前のトラック"
|
||||
msgid "Profile"
|
||||
msgstr "プロフィール"
|
||||
|
||||
#: src/features/badges/components/badge-showcase.tsx:301
|
||||
#: src/features/badges/components/badge-showcase.tsx:302
|
||||
msgid "Progress "
|
||||
msgstr "進行状況 "
|
||||
|
||||
@@ -2116,13 +2112,13 @@ msgstr "クエスト"
|
||||
msgid "Re-roll"
|
||||
msgstr "再抽選"
|
||||
|
||||
#: src/features/reactions/components/emoji-bar.tsx:213
|
||||
#: src/features/reactions/components/emoji-bar.tsx:210
|
||||
msgid "Reactions"
|
||||
msgstr "リアクション"
|
||||
|
||||
#. placeholder {0}: migrationStatus.playersProcessed
|
||||
#. placeholder {1}: migrationStatus.totalPlayers
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:72
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:75
|
||||
msgid "Recalculating badges in the background ({0}/{1})"
|
||||
msgstr "バックグラウンドでバッジを再計算中({0}/{1})"
|
||||
|
||||
@@ -2130,8 +2126,8 @@ msgstr "バックグラウンドでバッジを再計算中({0}/{1})"
|
||||
msgid "Recently finished"
|
||||
msgstr "最近終了した試合"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:79
|
||||
#: src/features/players/components/profile/index.tsx:37
|
||||
#: src/app/routes/_authed/stats.tsx:82
|
||||
#: src/features/players/components/profile/index.tsx:39
|
||||
msgid "Regional"
|
||||
msgstr "地域"
|
||||
|
||||
@@ -2182,7 +2178,7 @@ msgid "rofl"
|
||||
msgstr "大笑い"
|
||||
|
||||
#. placeholder {0}: match.round + 1
|
||||
#: src/features/matches/components/match-card.tsx:79
|
||||
#: src/features/matches/components/match-card.tsx:82
|
||||
msgid "Round {0}"
|
||||
msgstr "ラウンド{0}"
|
||||
|
||||
@@ -2245,8 +2241,8 @@ msgstr "曲を検索..."
|
||||
msgid "Search for teams to enroll..."
|
||||
msgstr "登録するチームを検索..."
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:132
|
||||
#: src/features/players/components/player-stats-table.tsx:332
|
||||
#: src/features/players/components/league-head-to-head.tsx:137
|
||||
#: src/features/players/components/player-stats-table.tsx:331
|
||||
msgid "Search players"
|
||||
msgstr "プレイヤーを検索"
|
||||
|
||||
@@ -2283,7 +2279,7 @@ msgstr "バッジを選択"
|
||||
msgid "Select a device..."
|
||||
msgstr "デバイスを選択..."
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:112
|
||||
#: src/features/players/components/league-head-to-head.tsx:117
|
||||
msgid "Select a player"
|
||||
msgstr "プレイヤーを選択"
|
||||
|
||||
@@ -2366,7 +2362,7 @@ msgstr "サーバー関数"
|
||||
msgid "serverFn name"
|
||||
msgstr "serverFn名"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:54
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:60
|
||||
#: src/features/telemetry/components/usage-screen.tsx:212
|
||||
msgid "Sessions"
|
||||
msgstr "セッション数"
|
||||
@@ -2382,7 +2378,7 @@ msgstr "詳細を表示"
|
||||
|
||||
#. placeholder {0}: filteredAndSortedStats.length
|
||||
#. placeholder {1}: playerStats.length
|
||||
#: src/features/players/components/player-stats-table.tsx:329
|
||||
#: src/features/players/components/player-stats-table.tsx:328
|
||||
msgid "Showing {0} of {1} players"
|
||||
msgstr "{1}人中{0}人のプレイヤーを表示"
|
||||
|
||||
@@ -2434,7 +2430,7 @@ msgstr "曲のセグメントは15秒以内にしてください"
|
||||
msgid "Song start time is required"
|
||||
msgstr "曲の開始時間を入力してください"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:350
|
||||
#: src/features/players/components/player-stats-table.tsx:349
|
||||
#: src/features/telemetry/components/shared/search-filter-bar.tsx:102
|
||||
msgid "Sort:"
|
||||
msgstr "並び替え:"
|
||||
@@ -2489,21 +2485,21 @@ msgstr "入力して検索..."
|
||||
msgid "Starts in"
|
||||
msgstr "開始まで"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:382
|
||||
#: src/features/players/components/player-stats-table.tsx:381
|
||||
msgid "Stat abbreviations and MMR info"
|
||||
msgstr "スタッツ略語とMMR情報"
|
||||
|
||||
#: src/features/players/components/player-stats-table.tsx:389
|
||||
#: src/features/players/components/player-stats-table.tsx:388
|
||||
msgid "Stat Abbreviations:"
|
||||
msgstr "スタッツ略語:"
|
||||
|
||||
#: src/features/core/hooks/use-links.ts:17
|
||||
#: src/features/players/components/profile/index.tsx:33
|
||||
#: src/features/players/components/profile/index.tsx:35
|
||||
#: src/features/teams/components/team-profile/index.tsx:34
|
||||
msgid "Statistics"
|
||||
msgstr "統計"
|
||||
|
||||
#: src/app/routes/_authed/stats.tsx:52
|
||||
#: src/app/routes/_authed/stats.tsx:55
|
||||
msgid "Stats"
|
||||
msgstr "スタッツ"
|
||||
|
||||
@@ -2520,7 +2516,7 @@ msgstr "ステータス"
|
||||
#: src/components/sheet/slide-panel/slide-panel.tsx:40
|
||||
#: src/features/login/components/player-prompt/index.tsx:115
|
||||
#: src/features/login/components/player-prompt/index.tsx:126
|
||||
#: src/features/predictions/components/prediction-editor.tsx:240
|
||||
#: src/features/predictions/components/prediction-editor.tsx:244
|
||||
#: src/features/tournaments/components/edit-rules.tsx:39
|
||||
msgid "Submit"
|
||||
msgstr "送信"
|
||||
@@ -2543,7 +2539,7 @@ msgstr "システム"
|
||||
msgid "T"
|
||||
msgstr "T"
|
||||
|
||||
#: src/features/players/components/league-head-to-head.tsx:92
|
||||
#: src/features/players/components/league-head-to-head.tsx:97
|
||||
msgid "Tap an opponent below to compare"
|
||||
msgstr "下の対戦相手をタップして比較"
|
||||
|
||||
@@ -2581,7 +2577,7 @@ msgstr "チーム詳細"
|
||||
msgid "Team enrolled successfully!"
|
||||
msgstr "チームを登録しました!"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:93
|
||||
#: src/app/routes/api/teams/upload-logo.ts:94
|
||||
#: src/features/teams/components/team-profile/index.tsx:21
|
||||
msgid "Team not found"
|
||||
msgstr "チームが見つかりません"
|
||||
@@ -2603,7 +2599,7 @@ msgstr "チームを更新しましたが、ロゴのアップロードに失敗
|
||||
msgid "Team updated successfully!"
|
||||
msgstr "チームを更新しました!"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:90
|
||||
#: src/features/players/components/profile/index.tsx:92
|
||||
#: src/features/players/components/profile/skeleton.tsx:75
|
||||
#: src/features/tournaments/components/profile/index.tsx:41
|
||||
#: src/features/tournaments/components/profile/skeleton.tsx:89
|
||||
@@ -2615,8 +2611,8 @@ msgstr "チーム"
|
||||
msgid "Teams created successfully!"
|
||||
msgstr "チームを作成しました!"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:20
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:55
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:26
|
||||
#: src/features/admin/components/dashboard/tools-list.tsx:57
|
||||
msgid "Telemetry"
|
||||
msgstr "テレメトリー"
|
||||
|
||||
@@ -2730,7 +2726,7 @@ msgstr "トーナメントを作成しましたが、ロゴのアップロード
|
||||
msgid "Tournament created successfully!"
|
||||
msgstr "トーナメントを作成しました!"
|
||||
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:94
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:95
|
||||
msgid "Tournament not found"
|
||||
msgstr "トーナメントが見つかりません"
|
||||
|
||||
@@ -2784,8 +2780,8 @@ msgstr "チームを登録解除"
|
||||
msgid "Unknown error"
|
||||
msgstr "不明なエラー"
|
||||
|
||||
#: src/app/routes/api/teams/upload-logo.ts:142
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:140
|
||||
#: src/app/routes/api/teams/upload-logo.ts:143
|
||||
#: src/app/routes/api/tournaments/upload-logo.ts:141
|
||||
msgid "Unknown error occurred"
|
||||
msgstr "不明なエラーが発生しました"
|
||||
|
||||
@@ -2810,7 +2806,7 @@ msgstr "開催予定"
|
||||
msgid "Update Match"
|
||||
msgstr "試合を更新"
|
||||
|
||||
#: src/features/players/components/profile/header.tsx:89
|
||||
#: src/features/players/components/profile/header.tsx:74
|
||||
msgid "Update Name"
|
||||
msgstr "名前を更新"
|
||||
|
||||
@@ -2824,7 +2820,7 @@ msgstr "チームを更新"
|
||||
msgid "Update Tournament"
|
||||
msgstr "トーナメントを更新"
|
||||
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:45
|
||||
#: src/app/routes/_authed/admin/telemetry.tsx:51
|
||||
msgid "Usage"
|
||||
msgstr "利用状況"
|
||||
|
||||
@@ -2837,7 +2833,7 @@ msgstr "ユーザーエージェント"
|
||||
msgid "versus"
|
||||
msgstr "対"
|
||||
|
||||
#: src/features/players/components/profile/index.tsx:73
|
||||
#: src/features/players/components/profile/index.tsx:75
|
||||
msgid "View all badges"
|
||||
msgstr "すべてのバッジを見る"
|
||||
|
||||
@@ -2853,8 +2849,8 @@ msgstr "ブラケットを見る"
|
||||
msgid "View Groups"
|
||||
msgstr "グループを見る"
|
||||
|
||||
#: src/features/bracket/components/match-dock.tsx:182
|
||||
#: src/features/matches/components/match-card.tsx:91
|
||||
#: src/features/bracket/components/match-dock.tsx:187
|
||||
#: src/features/matches/components/match-card.tsx:94
|
||||
msgid "View head-to-head"
|
||||
msgstr "対戦成績を見る"
|
||||
|
||||
@@ -2935,7 +2931,7 @@ msgstr "勝者ブラケット決勝"
|
||||
|
||||
#: src/components/stats-overview.tsx:103
|
||||
#: src/components/stats-overview.tsx:136
|
||||
#: src/features/players/components/player-stats-table.tsx:366
|
||||
#: src/features/players/components/player-stats-table.tsx:365
|
||||
msgid "Wins"
|
||||
msgstr "勝利数"
|
||||
|
||||
|
||||
@@ -178,15 +178,14 @@
|
||||
/* 3. Fixed sky backdrop with static bubbles (below all content) */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
[data-theme="aero"] body::before {
|
||||
content: "";
|
||||
[data-theme="aero"] .flxn-theme-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-theme="aero"][data-mantine-color-scheme="light"] body::before {
|
||||
[data-theme="aero"][data-mantine-color-scheme="light"] .flxn-theme-overlay {
|
||||
background-color: #e9f6fe;
|
||||
background-image:
|
||||
radial-gradient(circle 0.6rem at 13.5% 20%, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0) 70%),
|
||||
@@ -198,7 +197,7 @@
|
||||
linear-gradient(180deg, #a5daf6 0%, #c9e9fa 32%, #e9f6fe 60%, #ffffff 100%);
|
||||
}
|
||||
|
||||
[data-theme="aero"][data-mantine-color-scheme="dark"] body::before {
|
||||
[data-theme="aero"][data-mantine-color-scheme="dark"] .flxn-theme-overlay {
|
||||
background-color: #071e2d;
|
||||
background-image:
|
||||
radial-gradient(circle 4rem at 16% 24%, rgba(120, 205, 245, 0) 60%, rgba(120, 205, 245, 0.2) 67%, rgba(120, 205, 245, 0) 74%),
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
explicit [data-mantine-color-scheme] blocks. */
|
||||
|
||||
:root {
|
||||
/* bottom-nav indicator travel: spring (stiffness 320, damping 30) */
|
||||
--flxn-nav-travel: linear(0, 0.0102, 0.0376, 0.0778, 0.1272, 0.1829, 0.2422, 0.3033, 0.3646, 0.4247, 0.4829, 0.5383, 0.5906, 0.6393, 0.6843, 0.7256, 0.7631, 0.7969, 0.8273, 0.8543, 0.8783, 0.8993, 0.9176, 0.9335, 0.9472, 0.9589, 0.9688, 0.9772, 0.9841, 0.9898, 0.9945, 0.9982, 1.0011, 1.0034, 1.0051, 1.0063, 1.0072, 1.0077, 1.0079, 1.0079, 1.0078);
|
||||
--flxn-nav-travel-ms: 333ms;
|
||||
|
||||
/* accolades */
|
||||
--flxn-gold: gold;
|
||||
--flxn-silver: var(--mantine-color-gray-5);
|
||||
|
||||
@@ -203,8 +203,7 @@
|
||||
|
||||
/* ---------- 4. Backdrop — studio key light + clay grain ---------- */
|
||||
|
||||
[data-theme="clay"] body::before {
|
||||
content: "";
|
||||
[data-theme="clay"] .flxn-theme-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1; /* behind content — never tints text or surfaces */
|
||||
@@ -212,7 +211,7 @@
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
[data-theme="clay"][data-mantine-color-scheme="light"] body::before {
|
||||
[data-theme="clay"][data-mantine-color-scheme="light"] .flxn-theme-overlay {
|
||||
background-image: radial-gradient(
|
||||
120% 70% at 50% 0%,
|
||||
rgba(255, 252, 244, 0.65),
|
||||
@@ -221,7 +220,7 @@
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
[data-theme="clay"][data-mantine-color-scheme="dark"] body::before {
|
||||
[data-theme="clay"][data-mantine-color-scheme="dark"] .flxn-theme-overlay {
|
||||
background-image: radial-gradient(
|
||||
110% 65% at 50% 0%,
|
||||
rgba(255, 208, 158, 0.09),
|
||||
|
||||
@@ -363,8 +363,7 @@
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 4. Sunburst fan — faint gold rays from top center */
|
||||
/* ------------------------------------------------------------------ */
|
||||
[data-theme="deco"] body::before {
|
||||
content: "";
|
||||
[data-theme="deco"] .flxn-theme-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9998;
|
||||
|
||||
@@ -409,8 +409,7 @@
|
||||
* 3. Full-page decoration: candlelit vignette (dark scheme only)
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
[data-theme="gothic"][data-mantine-color-scheme="dark"] body::before {
|
||||
content: "";
|
||||
[data-theme="gothic"][data-mantine-color-scheme="dark"] .flxn-theme-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9998;
|
||||
|
||||
@@ -67,3 +67,61 @@
|
||||
border: 1px solid var(--mantine-color-default-border);
|
||||
border-radius: var(--mantine-radius-md);
|
||||
}
|
||||
|
||||
/* Bottom-nav active indicator: one element glides between tabs. The surface is
|
||||
neutral on purpose — the accent belongs to the glyph, not a tint behind it. */
|
||||
.flxn-nav-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: var(--nav-w, 0);
|
||||
height: var(--nav-h, 0);
|
||||
translate: var(--nav-x, 0) var(--nav-y, 0);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
z-index: 0;
|
||||
transition: translate var(--flxn-nav-travel-ms) var(--flxn-nav-travel),
|
||||
width var(--flxn-nav-travel-ms) var(--flxn-nav-travel),
|
||||
height var(--flxn-nav-travel-ms) var(--flxn-nav-travel),
|
||||
opacity 140ms ease-out;
|
||||
}
|
||||
.flxn-nav-indicator[data-placed="1"] {
|
||||
opacity: 1;
|
||||
}
|
||||
.flxn-nav-indicator-shape {
|
||||
display: block;
|
||||
width: calc(100% + var(--flxn-nav-pad-x, 12px));
|
||||
height: calc(100% + var(--flxn-nav-pad-y, 6px));
|
||||
margin: calc(var(--flxn-nav-pad-y, 6px) / -2) calc(var(--flxn-nav-pad-x, 12px) / -2);
|
||||
background: var(--flxn-nav-surface, var(--mantine-color-default));
|
||||
border: 1px solid var(--flxn-nav-border, var(--mantine-color-default-border));
|
||||
border-radius: var(--flxn-nav-radius, var(--mantine-radius-md));
|
||||
box-shadow: var(--mantine-shadow-xs);
|
||||
}
|
||||
.flxn-nav-link {
|
||||
/* lifted above the indicator, or an opaque fill covers the glyph */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.flxn-nav-link svg,
|
||||
.flxn-nav-link p {
|
||||
transition: color 140ms ease-out;
|
||||
}
|
||||
.flxn-nav-link[data-active] {
|
||||
color: var(--flxn-nav-accent, var(--mantine-primary-color-filled));
|
||||
}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.flxn-nav-link[data-active] svg {
|
||||
animation: flxn-nav-pop 320ms cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
}
|
||||
@keyframes flxn-nav-pop {
|
||||
0% { scale: 1; }
|
||||
38% { scale: 1.16; }
|
||||
100% { scale: 1; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.flxn-nav-indicator {
|
||||
transition: opacity 120ms ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,8 +153,7 @@
|
||||
/* 3. Fixed backdrop (below all content) */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
[data-theme="ocean"] body::before {
|
||||
content: "";
|
||||
[data-theme="ocean"] .flxn-theme-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
@@ -163,7 +162,7 @@
|
||||
|
||||
/* Abyssal descent: deep teal fading to ink navy, sparse static bubbles
|
||||
rising (bigger and brighter near the sea floor, fading toward the top). */
|
||||
[data-theme="ocean"][data-mantine-color-scheme="dark"] body::before {
|
||||
[data-theme="ocean"][data-mantine-color-scheme="dark"] .flxn-theme-overlay {
|
||||
background-color: #062632;
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='640' viewBox='0 0 260 640'%3E%3Cg fill='none' stroke='%239beee2'%3E%3Ccircle cx='36' cy='584' r='5' stroke-opacity='.13'/%3E%3Ccircle cx='44' cy='512' r='3' stroke-opacity='.1'/%3E%3Ccircle cx='210' cy='612' r='4' stroke-opacity='.12'/%3E%3Ccircle cx='202' cy='540' r='2.5' stroke-opacity='.09'/%3E%3Ccircle cx='128' cy='430' r='3.5' stroke-opacity='.1'/%3E%3Ccircle cx='120' cy='360' r='2' stroke-opacity='.08'/%3E%3Ccircle cx='232' cy='300' r='3' stroke-opacity='.09'/%3E%3Ccircle cx='60' cy='250' r='2.5' stroke-opacity='.08'/%3E%3Ccircle cx='150' cy='160' r='2' stroke-opacity='.07'/%3E%3Ccircle cx='90' cy='80' r='1.5' stroke-opacity='.06'/%3E%3C/g%3E%3Cg fill='%239beee2'%3E%3Ccircle cx='70' cy='470' r='1.2' fill-opacity='.1'/%3E%3Ccircle cx='180' cy='390' r='1' fill-opacity='.09'/%3E%3Ccircle cx='40' cy='330' r='1' fill-opacity='.08'/%3E%3Ccircle cx='222' cy='190' r='1' fill-opacity='.07'/%3E%3Ccircle cx='140' cy='40' r='1' fill-opacity='.05'/%3E%3C/g%3E%3C/svg%3E"),
|
||||
@@ -175,7 +174,7 @@
|
||||
|
||||
/* Sunlit shallows: seafoam fading to a sandy floor, with a faint white
|
||||
caustic ripple tile — refracted light, kept very subtle. */
|
||||
[data-theme="ocean"][data-mantine-color-scheme="light"] body::before {
|
||||
[data-theme="ocean"][data-mantine-color-scheme="light"] .flxn-theme-overlay {
|
||||
background-color: #eaf6f6;
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='220' viewBox='0 0 320 220'%3E%3Cg fill='none' stroke='%23ffffff' stroke-width='1.4'%3E%3Cpath d='M0 40 Q 40 10 80 42 T 160 44 T 240 38 T 320 42' stroke-opacity='.4'/%3E%3Cpath d='M0 110 Q 45 82 90 112 T 180 110 T 268 114 T 320 108' stroke-opacity='.3'/%3E%3Cpath d='M0 180 Q 50 152 100 182 T 200 178 T 300 184 T 320 180' stroke-opacity='.35'/%3E%3C/g%3E%3C/svg%3E"),
|
||||
|
||||
@@ -414,8 +414,7 @@
|
||||
* 3. Full-page decoration: worn parchment edges / torchlight
|
||||
* ---------------------------------------------------------------- */
|
||||
|
||||
[data-theme="quest"][data-mantine-color-scheme="light"] body::before {
|
||||
content: "";
|
||||
[data-theme="quest"][data-mantine-color-scheme="light"] .flxn-theme-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9998;
|
||||
@@ -426,8 +425,7 @@
|
||||
radial-gradient(55% 38% at 95% 98%, rgba(122, 91, 42, 0.07), transparent 70%);
|
||||
}
|
||||
|
||||
[data-theme="quest"][data-mantine-color-scheme="dark"] body::before {
|
||||
content: "";
|
||||
[data-theme="quest"][data-mantine-color-scheme="dark"] .flxn-theme-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9998;
|
||||
|
||||
@@ -151,8 +151,7 @@
|
||||
/* 3. Paper grain / chalk dust — one fixed overlay, feTurbulence */
|
||||
/* ---------------------------------------------------------------- */
|
||||
|
||||
[data-theme="sketch"] body::before {
|
||||
content: "";
|
||||
[data-theme="sketch"] .flxn-theme-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9998;
|
||||
@@ -163,7 +162,7 @@
|
||||
opacity: 0.05;
|
||||
}
|
||||
|
||||
[data-theme="sketch"][data-mantine-color-scheme="dark"] body::before {
|
||||
[data-theme="sketch"][data-mantine-color-scheme="dark"] .flxn-theme-overlay {
|
||||
mix-blend-mode: screen;
|
||||
opacity: 0.07;
|
||||
}
|
||||
|
||||
@@ -575,8 +575,7 @@
|
||||
}
|
||||
|
||||
/* 4. Decoration: fixed scanline overlay + faint RGB aperture mask */
|
||||
[data-theme="terminal"] body::before {
|
||||
content: "";
|
||||
[data-theme="terminal"] .flxn-theme-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9998;
|
||||
@@ -589,7 +588,7 @@
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
[data-theme="terminal"] body::before {
|
||||
[data-theme="terminal"] .flxn-theme-overlay {
|
||||
animation: terminal-phosphor-drift 9s ease-in-out infinite;
|
||||
}
|
||||
@keyframes terminal-phosphor-drift {
|
||||
|
||||
@@ -150,8 +150,7 @@
|
||||
|
||||
/* ---------- 3. Decoration: static perspective floor-grid hint ---------- */
|
||||
|
||||
[data-theme="wireframe"] body::before {
|
||||
content: "";
|
||||
[data-theme="wireframe"] .flxn-theme-overlay {
|
||||
position: fixed;
|
||||
left: -30vw;
|
||||
right: -30vw;
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from "@tanstack/react-start";
|
||||
import { getRequest } from "@tanstack/react-start/server";
|
||||
import { Logger } from "@/lib/logger";
|
||||
import { isAdminRole } from "@/features/core/utils/roles";
|
||||
import { isAdminRole, isDeveloperRole } from "@/features/core/utils/roles";
|
||||
import z from "zod";
|
||||
|
||||
const logger = new Logger("Middleware");
|
||||
@@ -70,7 +70,11 @@ export const superTokensFunctionMiddleware = createMiddleware({
|
||||
}
|
||||
});
|
||||
|
||||
export const superTokensAdminFunctionMiddleware = createMiddleware({
|
||||
const createRoleFunctionMiddleware = (
|
||||
hasRole: (roles?: string[] | null) => boolean,
|
||||
label: string
|
||||
) =>
|
||||
createMiddleware({
|
||||
type: "function",
|
||||
}).server(async ({ next, serverFnMeta }) => {
|
||||
const request = getRequest();
|
||||
@@ -78,11 +82,11 @@ export const superTokensAdminFunctionMiddleware = createMiddleware({
|
||||
try {
|
||||
const context = await getSessionContext(request, { isServerFunction: true });
|
||||
|
||||
if (isAdminRole(context.roles)) {
|
||||
if (hasRole(context.roles)) {
|
||||
return next({ context });
|
||||
}
|
||||
|
||||
logger.error("Unauthorized user in admin function.", {
|
||||
logger.error(`Unauthorized user in ${label} function.`, {
|
||||
userAuthId: context.userAuthId,
|
||||
roles: context.roles,
|
||||
});
|
||||
@@ -106,6 +110,16 @@ export const superTokensAdminFunctionMiddleware = createMiddleware({
|
||||
}
|
||||
});
|
||||
|
||||
export const superTokensAdminFunctionMiddleware = createRoleFunctionMiddleware(
|
||||
isAdminRole,
|
||||
"admin"
|
||||
);
|
||||
|
||||
export const superTokensDeveloperFunctionMiddleware = createRoleFunctionMiddleware(
|
||||
isDeveloperRole,
|
||||
"developer"
|
||||
);
|
||||
|
||||
export const setUserMetadata = createServerFn({ method: "POST" })
|
||||
.validator(
|
||||
z
|
||||
|
||||
Reference in New Issue
Block a user