3 Commits
Author SHA1 Message Date
kyle 2ae6269a15 Merge pull request 'Development' (#24) from development into main
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 14s
CI/CD Pipeline / i18n Catalog Check (push) Successful in 55s
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 3m34s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 8m26s
Reviewed-on: #24
2026-08-28 10:40:06 -05:00
kyle f4396d9527 Merge pull request 'pwa fixes' (#23) from development into main
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 8s
CI/CD Pipeline / i18n Catalog Check (push) Successful in 43s
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 2m4s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 8m28s
Reviewed-on: #23
2026-08-24 00:04:44 -05:00
kyle 352fadadb5 Merge pull request 'Adding: themes, predictions, match reporting, ui fixes, sse fixes, badge fixes, match flow, i18n, match edits, perf, auth fix' (#22) from development into main
CI/CD Pipeline / i18n Catalog Check (push) Successful in 30s
CI/CD Pipeline / Build and Push PocketBase Docker Image (push) Successful in 36s
CI/CD Pipeline / Build and Push App Docker Image (push) Successful in 1m53s
CI/CD Pipeline / Deploy to Kubernetes (push) Successful in 8m30s
Reviewed-on: #22
2026-08-23 23:08:56 -05:00
59 changed files with 468 additions and 612 deletions
+9 -9
View File
@@ -31,8 +31,8 @@
"facehash": "^0.0.7",
"framer-motion": "^12.23.12",
"pocketbase": "^0.26.2",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"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.3.0",
"@types/react-dom": "^19.3.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@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.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
"@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@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.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="],
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
"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-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
"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.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"scmp": ["scmp@2.1.0", "", {}, "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q=="],
+4 -4
View File
@@ -43,8 +43,8 @@
"facehash": "^0.0.7",
"framer-motion": "^12.23.12",
"pocketbase": "^0.26.2",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"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.3.0",
"@types/react-dom": "^19.3.0",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@types/web-push": "^3.6.4",
"@vitejs/plugin-react": "^5.0.0",
"autoprefixer": "^10.4.20",
-6
View File
@@ -50,12 +50,6 @@ 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] {
+1
View File
@@ -13,6 +13,7 @@ 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
+8 -17
View File
@@ -1,5 +1,5 @@
// 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]
// Grant or revoke the "Admin" role for a user by phone number (US +1 assumed).
// Usage: bun run scripts/make-admin.ts <phone> [--revoke]
import "dotenv/config";
import SuperTokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
@@ -26,19 +26,10 @@ 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> [--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(", ")}`);
console.error("Usage: bun run scripts/make-admin.ts <phone> [--revoke]");
process.exit(1);
}
@@ -69,15 +60,15 @@ if (!user) {
process.exit(1);
}
await UserRoles.createNewRoleOrAddPermissions(role, []);
await UserRoles.createNewRoleOrAddPermissions("Admin", []);
if (revoke) {
const res = await UserRoles.removeUserRole("public", user.id, role);
console.log(`Removed ${role} from ${matched} (user ${user.id}):`, res.status);
const res = await UserRoles.removeUserRole("public", user.id, "Admin");
console.log(`Removed Admin from ${matched} (user ${user.id}):`, res.status);
} else {
const res = await UserRoles.addRoleToUser("public", user.id, role);
const res = await UserRoles.addRoleToUser("public", user.id, "Admin");
console.log(
`Granted ${role} to ${matched} (user ${user.id}):`,
`Granted Admin to ${matched} (user ${user.id}):`,
res.status === "OK"
? res.didUserAlreadyHaveRole
? "already had it"
-4
View File
@@ -3,7 +3,6 @@ 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() {
@@ -31,9 +30,6 @@ export function getRouter() {
},
defaultPreload: "intent",
defaultPreloadStaleTime: 60_000,
defaultPendingMs: 100,
defaultPendingMinMs: 0,
defaultPendingComponent: RoutePending,
defaultErrorComponent: DefaultCatchBoundary,
});
+2 -16
View File
@@ -31,7 +31,6 @@ 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'
@@ -102,7 +101,6 @@ 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 },
@@ -185,19 +183,7 @@ function RootComponent() {
ensureSuperTokensFrontend();
initTelemetry(router);
if (import.meta.env.PROD && 'serviceWorker' in navigator) {
// 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'));
navigator.serviceWorker.register('/sw.js').catch(() => track('sw_register_failed'));
}
}, [router]);
@@ -245,6 +231,7 @@ 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={{
@@ -256,7 +243,6 @@ function RootDocument({ children }: { children: React.ReactNode }) {
}}
>
<div className="app">{children}</div>
<div className="flxn-theme-overlay" aria-hidden="true" />
<Scripts />
</body>
</html>
+1 -7
View File
@@ -1,8 +1,7 @@
import { Outlet, createFileRoute, redirect, useNavigate, useRouterState } from "@tanstack/react-router";
import { Outlet, createFileRoute, 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",
@@ -16,11 +15,6 @@ 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`,
+7 -5
View File
@@ -1,8 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";
import { tournamentQueries, useCurrentTournament } from "@/features/tournaments/queries";
import UpcomingTournament from "@/features/tournaments/components/upcoming-tournament";
import { getServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
import StartedTournament from "@/features/tournaments/components/started-tournament";
import { Suspense } from "react";
import UpcomingTournamentSkeleton from "@/features/tournaments/components/upcoming-tournament/skeleton";
@@ -11,13 +10,16 @@ export const Route = createFileRoute("/_authed/")({
component: () => <Suspense fallback={<UpcomingTournamentSkeleton />}>
<Home />
</Suspense>,
beforeLoad: ({ context }) => {
prefetchServerQuery(context.queryClient, tournamentQueries.current());
beforeLoad: async ({ context }) => {
const queryClient = context.queryClient;
const tournament = await ensureServerQueryData(queryClient, tournamentQueries.current())
return { tournament }
},
loader: ({ context }) => ({
withPadding: false,
header: {
title: getServerQueryData(context.queryClient, tournamentQueries.current())?.name || "FLXN"
title: context.tournament.name || "FLXN"
}
}),
pendingComponent: () => <UpcomingTournamentSkeleton />
+19 -5
View File
@@ -14,12 +14,26 @@ const searchSchema = z.object({
export const Route = createFileRoute("/_authed/profile/$playerId")({
validateSearch: searchSchema,
beforeLoad: ({ params, context }) => {
beforeLoad: async ({ params, context }) => {
const { queryClient } = context;
prefetchServerQuery(queryClient, playerQueries.details(params.playerId));
prefetchServerQuery(queryClient, playerQueries.matches(params.playerId));
prefetchServerQuery(queryClient, playerQueries.stats(params.playerId));
prefetchServerQuery(queryClient, badgeQueries.playerBadges(params.playerId));
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)
),
]);
},
loader: ({ params, context }) => ({
header: {
+2 -2
View File
@@ -12,9 +12,9 @@ const searchSchema = z.object({
export const Route = createFileRoute("/_authed/teams/$teamId")({
validateSearch: searchSchema,
beforeLoad: ({ params, context }) => {
beforeLoad: async ({ params, context }) => {
const { queryClient } = context;
prefetchServerQuery(queryClient, teamQueries.details(params.teamId));
await prefetchServerQuery(queryClient, teamQueries.details(params.teamId));
},
loader: ({ params }) => ({
header: {
+1 -2
View File
@@ -1,7 +1,6 @@
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";
@@ -20,7 +19,7 @@ export const Route = createFileRoute("/api/teams/upload-logo")({
const i18n = localizedFor(context.metadata);
try {
const userId = context.userAuthId;
const isAdmin = isAdminRole(context.roles);
const isAdmin = context.roles.includes("Admin");
if (!userId) return new Response("Unauthenticated", { status: 401 });
@@ -1,7 +1,6 @@
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";
@@ -20,7 +19,7 @@ export const Route = createFileRoute("/api/tournaments/upload-logo")({
const i18n = localizedFor(context.metadata);
try {
const userId = context.userAuthId;
const isAdmin = isAdminRole(context.roles);
const isAdmin = context.roles.includes("Admin");
if (!userId) return new Response("Unauthenticated", { status: 401 });
if (!isAdmin) return new Response("Unauthorized", { status: 403 });
-35
View File
@@ -1,35 +0,0 @@
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;
-17
View File
@@ -1,17 +0,0 @@
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;
+2 -8
View File
@@ -6,7 +6,7 @@ import {
ReactNode,
useRef,
useCallback,
startTransition,
useMemo,
} from "react";
import { useRouter } from "@tanstack/react-router";
@@ -48,9 +48,6 @@ 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);
@@ -63,9 +60,6 @@ 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]);
@@ -231,7 +225,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" }}>
{(visitedTabs.has(index) || index === activeTab) ? tab.content : null}
{tab.content}
</Box>
</Carousel.Slide>
))}
+2 -2
View File
@@ -1,4 +1,4 @@
import { superTokensDeveloperFunctionMiddleware } from "@/utils/supertokens";
import { superTokensAdminFunctionMiddleware } 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([superTokensDeveloperFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data }) =>
toServerResult<ActivityListResult>(async () => {
const result = await pbAdmin.searchActivities(data);
@@ -1,6 +1,7 @@
import { List, Text } from "@mantine/core";
import { Plural, Trans, useLingui } from "@lingui/react/macro";
import {
DatabaseIcon,
TreeStructureIcon,
TrophyIcon,
MedalIcon,
@@ -9,14 +10,12 @@ 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"],
@@ -52,13 +51,11 @@ 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}
@@ -94,6 +91,13 @@ 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,7 +49,6 @@ export const BadgeIcon = ({ badge, filled, size = 48 }: BadgeIconProps & { size?
width={size}
height={size}
loading="lazy"
decoding="async"
onError={() => setImageError(true)}
style={{
objectFit: 'contain',
+23 -2
View File
@@ -13,7 +13,7 @@ import {
Tooltip,
} from "@mantine/core";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import HeadToHeadIcon from "@/components/head-to-head-icon";
import { FootballHelmetIcon } from "@phosphor-icons/react";
import { useNavigate } from "@tanstack/react-router";
import { Match } from "@/features/matches/types";
import TeamAvatar from "@/components/team-avatar";
@@ -188,7 +188,28 @@ const MatchDock = ({ match, onClose }: MatchDockProps) => {
w={40}
style={{ flexShrink: 0 }}
>
<HeadToHeadIcon />
<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>
</ActionIcon>
</Tooltip>
)}
+7 -4
View File
@@ -1,22 +1,23 @@
import { AppShell } from '@mantine/core';
import { AppShell, Box } 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.
@@ -47,14 +48,16 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
pos='relative'
h='100%'
mah='100%'
pb={{ base: 'calc(73px + env(safe-area-inset-bottom, 0px))', sm: 0 }}
pb={{ base: 'calc(65px + 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>
@@ -1,68 +0,0 @@
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,19 +32,30 @@ export const NavLink = ({
<Box
component={Link}
to={href}
className={`flxn-nav-link ${styles.navLinkBox}`}
className={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} />
<Text visibleFrom="md" ta="center" size="md" fw={isActive ? 800 : 500}>
<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}
>
{label}
</Text>
</Flex>
@@ -11,6 +11,11 @@
-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);
+4 -14
View File
@@ -5,13 +5,11 @@ 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);
@@ -19,16 +17,9 @@ 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 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}
<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 }}>
{links.map((link) => (
<NavLink key={link.href} {...link} />
))}
@@ -36,10 +27,9 @@ const Navbar = () => {
</Paper>
)
return <AppShell.Navbar className="flxn-nav" p="xs" role='navigation'>
return <AppShell.Navbar p="xs" role='navigation'>
<AppShell.Section grow component={ScrollArea}>
<Stack ref={containerRef} pos='relative' gap='xs' mx='auto' w='fit-content' justify='end' mt='md'>
{indicator}
<Stack gap='xs' mx='auto' w='fit-content' justify='end' mt='md'>
{links.map((link) => (
<NavLink key={link.href} {...link} />
))}
@@ -0,0 +1,11 @@
@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;
}
}
@@ -1,7 +0,0 @@
import { useAuth } from "@/contexts/auth-context";
import { isDeveloperRole } from "../utils/roles";
export const useIsDeveloper = (): boolean => {
const { roles } = useAuth();
return isDeveloperRole(roles);
};
+1 -5
View File
@@ -1,8 +1,4 @@
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) || isDeveloperRole(roles);
!!roles?.includes(ADMIN_ROLE);
+1 -1
View File
@@ -21,7 +21,7 @@ const useConsumeCode = (onWrongCode: () => void) => {
} else {
queryClient.setQueryData(playerKeys.auth, response.data);
toast.success(t`Successfully logged in. Welcome back!`);
window.location.href = '/';
navigate({ to: '/' })
}
} else if (data.status === 'INCORRECT_USER_INPUT_CODE_ERROR') {
onWrongCode();
+1 -1
View File
@@ -21,7 +21,7 @@ const useCreateUser = () => {
...old,
user: data
}));
window.location.href = '/';
navigate({ to: '/' });
}
});
};
+21 -2
View File
@@ -1,5 +1,5 @@
import { Text, Group, Stack, Paper, Indicator, Box, Tooltip, ActionIcon } from "@mantine/core";
import HeadToHeadIcon from "@/components/head-to-head-icon";
import { FootballHelmetIcon } from "@phosphor-icons/react";
import { useNavigate } from "@tanstack/react-router";
import { Match } from "../types";
import TeamAvatar from "@/components/team-avatar";
@@ -94,7 +94,26 @@ const MatchCard = ({ match, hideH2H = false }: MatchCardProps) => {
aria-label={t`View head-to-head`}
w={40}
>
<HeadToHeadIcon />
<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>
</ActionIcon>
</Tooltip>
)}
@@ -1,4 +1,4 @@
import { MantineSpacing, Stack, Text } from "@mantine/core";
import { Stack, Text } from "@mantine/core";
import { useMemo } from "react";
import { Match } from "../types";
import MatchCard from "./match-card";
@@ -7,10 +7,9 @@ import { Trans } from "@lingui/react/macro";
interface MatchListProps {
matches: Match[];
hideH2H?: boolean;
p?: MantineSpacing;
}
const MatchList = ({ matches, hideH2H = false, p = "md" }: MatchListProps) => {
const MatchList = ({ matches, hideH2H = false }: MatchListProps) => {
const filteredMatches = useMemo(
() =>
[...(matches ?? [])]
@@ -28,7 +27,7 @@ const MatchList = ({ matches, hideH2H = false, p = "md" }: MatchListProps) => {
const isRegional = filteredMatches[0]?.tournament?.regional;
return (
<Stack p={p} gap="sm">
<Stack p="md" 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}>
<Text size="sm" fw={600} px="md">
<Trans>Match History (<Plural value={totalMatches} one="# match" other="# matches" />)</Trans>
</Text>
<MatchList matches={matches} hideH2H p={0} />
<MatchList matches={matches} hideH2H />
</Stack>
</Stack>
);
@@ -260,10 +260,10 @@ const PlayerHeadToHeadContent = ({
</Stack>
<Stack gap="xs">
<Text size="sm" fw={600}>
<Text size="sm" fw={600} px="md">
<Trans>Match History ({totalMatches})</Trans>
</Text>
<MatchList matches={matches} hideH2H p={0} />
<MatchList matches={matches} hideH2H />
</Stack>
</Stack>
);
@@ -1,8 +1,7 @@
import Sheet from "@/components/sheet/sheet";
import { useAuth } from "@/contexts/auth-context";
import { Flex, Title, ActionIcon, Stack, Button } from "@mantine/core";
import { PencilIcon } from "@phosphor-icons/react";
import HeadToHeadIcon from "@/components/head-to-head-icon";
import { Flex, Title, ActionIcon, Stack, Button, Box } from "@mantine/core";
import { PencilIcon, FootballHelmetIcon } from "@phosphor-icons/react";
import { useMemo } from "react";
import { useLingui } from "@lingui/react/macro";
import NameUpdateForm from "./name-form";
@@ -65,7 +64,26 @@ const Header = ({ player }: HeaderProps) => {
w={40}
display={!owner ? 'block' : 'none'}
>
<HeadToHeadIcon />
<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>
</ActionIcon>
</Flex>
</Flex>
@@ -7,8 +7,7 @@ import {
Stack,
Text,
} from "@mantine/core";
import { CaretLeftIcon, CaretRightIcon } from "@phosphor-icons/react";
import HeadToHeadIcon from "@/components/head-to-head-icon";
import { CaretLeftIcon, CaretRightIcon, InfoIcon } from "@phosphor-icons/react";
import WizardOrbIcon from "@/components/wizard-orb-icon";
import { Trans, useLingui } from "@lingui/react/macro";
import { useNavigate } from "@tanstack/react-router";
@@ -229,7 +228,7 @@ export const PredictionEditor: React.FC<PredictionEditorProps> = ({
disabled={!activeResolved?.home && !activeResolved?.away}
aria-label={t`Matchup details`}
>
<HeadToHeadIcon />
<InfoIcon size={16} />
</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 { useIsDeveloper } from "@/features/core/hooks/use-is-developer";
import { useIsAdmin } from "@/features/core/hooks/use-is-admin";
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 isDeveloper = useIsDeveloper();
const isAdmin = useIsAdmin();
const { t } = useLingui();
if (!supported || !configured) {
@@ -99,7 +99,7 @@ export function NotificationsSection() {
disabled={busy}
label={t`Enable notifications on this device`}
/>
{subscribed && isDeveloper && <TelemetryAlertsSwitch />}
{subscribed && isAdmin && <TelemetryAlertsSwitch />}
{subscribed && (
<Group>
<Button
+2 -2
View File
@@ -1,6 +1,6 @@
import { createServerFn } from "@tanstack/react-start";
import {
superTokensDeveloperFunctionMiddleware,
superTokensAdminFunctionMiddleware,
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([superTokensDeveloperFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ context, data }) => {
const { userAuthId, metadata } = context;
if (!userAuthId) return;
+2 -3
View File
@@ -1,5 +1,4 @@
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";
@@ -36,7 +35,7 @@ export const createTeam = createServerFn()
.handler(async ({ data, context }) =>
toServerResult(async () => {
const userId = context.userAuthId;
const isAdmin = isAdminRole(context.roles);
const isAdmin = context.roles.includes("Admin");
//if (!isAdmin && !data.players.includes(userId)) {
// throw new Error("You can only create teams that include yourself as a player");
@@ -58,7 +57,7 @@ export const updateTeam = createServerFn()
.handler(async ({ data: { id, updates }, context }) =>
toServerResult(async () => {
const userId = context.userAuthId;
const isAdmin = isAdminRole(context.roles);
const isAdmin = context.roles.includes("Admin");
const team = await pbAdmin.getTeam(id);
if (!team) {
+10 -10
View File
@@ -1,6 +1,6 @@
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";
import { superTokensDeveloperFunctionMiddleware } from "@/utils/supertokens";
import { superTokensAdminFunctionMiddleware } 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([superTokensDeveloperFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware])
.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([superTokensDeveloperFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data }) =>
toServerResult<TelemetryListResult<ClientError>>(async () => {
const result = await pbAdmin.searchClientErrors(data);
@@ -77,7 +77,7 @@ const errorGroupsSchema = z.object({
// not on the next rollup tick.
export const getClientErrorGroups = createServerFn()
.validator(errorGroupsSchema)
.middleware([superTokensDeveloperFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data }) =>
toServerResult<ClientErrorGroup[]>(async () => {
const { from, to, includeResolved = false, limit = 50 } = data;
@@ -141,7 +141,7 @@ export const getClientErrorGroups = createServerFn()
export const resolveClientErrorGroup = createServerFn({ method: "POST" })
.validator(z.object({ groupHash: z.string(), resolved: z.boolean().optional() }))
.middleware([superTokensDeveloperFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data }) =>
toServerResult<{ updated: number }>(async () => {
const updated = await pbAdmin.resolveErrorsByGroup(data.groupHash, data.resolved ?? true);
@@ -158,7 +158,7 @@ const rollupRangeSchema = z.object({
export const getRollupRange = createServerFn()
.validator(rollupRangeSchema)
.middleware([superTokensDeveloperFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data }) =>
toServerResult<RollupRow[]>(async () => pbAdmin.getRollupRange(data))
);
@@ -167,7 +167,7 @@ export const listTelemetryAlerts = createServerFn()
.validator(
z.object({ page: z.number().optional(), perPage: z.number().max(200).optional() })
)
.middleware([superTokensDeveloperFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data }) =>
toServerResult<TelemetryListResult<TelemetryAlert>>(async () =>
pbAdmin.listTelemetryAlerts(data.page, data.perPage)
@@ -197,7 +197,7 @@ export const listSessions = createServerFn()
perPage: z.number().max(100).optional(),
})
)
.middleware([superTokensDeveloperFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data }) =>
toServerResult<TelemetryListResult<SessionSummary>>(async () => {
const { days = 7, page = 1, perPage = 30 } = data;
@@ -285,7 +285,7 @@ const MAX_TIMELINE_ENTRIES = 500;
export const getSessionTimeline = createServerFn()
.validator(z.object({ sessionId: z.string().min(8).max(64) }))
.middleware([superTokensDeveloperFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware])
.handler(async ({ data }) =>
toServerResult<SessionTimeline>(async () => {
const events = await pbAdmin.searchClientEvents({
@@ -426,7 +426,7 @@ export const getSessionTimeline = createServerFn()
);
export const getTelemetryRuntimeStatus = createServerFn()
.middleware([superTokensDeveloperFunctionMiddleware])
.middleware([superTokensAdminFunctionMiddleware])
.handler(async () =>
toServerResult<TelemetryRuntimeStatus>(async () => {
ensureTelemetryScheduler();
+2 -3
View File
@@ -1,5 +1,4 @@
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";
@@ -47,7 +46,7 @@ export const getTournament = createServerFn()
.validator(z.string())
.middleware([superTokensFunctionMiddleware])
.handler(async ({ data: tournamentId, context }) => {
const isAdmin = isAdminRole(context.roles);
const isAdmin = context.roles.includes("Admin");
return toServerResult(() => pbAdmin.getTournament(tournamentId, isAdmin));
});
@@ -65,7 +64,7 @@ export const enrollTeam = createServerFn()
.handler(async ({ data: { tournamentId, teamId }, context }) =>
toServerResult(async () => {
const userId = context.userAuthId;
const isAdmin = isAdminRole(context.roles);
const isAdmin = context.roles.includes("Admin");
const team = await pbAdmin.getTeam(teamId);
if (!team) { throw new Error('Team not found'); }
-10
View File
@@ -1,16 +1,6 @@
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: {
+11 -11
View File
@@ -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 { DEVELOPER_ROLE } from "@/features/core/utils/roles";
import { ADMIN_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 DEVELOPER_CACHE_TTL_MS = 10 * 60 * 1000;
let developerPlayersCache: { playerIds: string[]; expiresAt: number } | null = null;
const ADMIN_CACHE_TTL_MS = 10 * 60 * 1000;
let adminPlayersCache: { playerIds: string[]; expiresAt: number } | null = null;
const getDeveloperPlayerIds = async (): Promise<string[]> => {
const getAdminPlayerIds = async (): Promise<string[]> => {
const now = Date.now();
if (developerPlayersCache && developerPlayersCache.expiresAt > now) {
return developerPlayersCache.playerIds;
if (adminPlayersCache && adminPlayersCache.expiresAt > now) {
return adminPlayersCache.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", DEVELOPER_ROLE);
const response = await UserRoles.getUsersThatHaveRole("public", ADMIN_ROLE);
const users = response.status === "OK" ? response.users : [];
const playerIds: string[] = [];
for (const userId of users) {
try {
// Opt-in: only developers who enabled telemetry alerts in settings.
// Opt-in: only admins 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 getDeveloperPlayerIds = async (): Promise<string[]> => {
} catch {}
}
developerPlayersCache = { playerIds, expiresAt: now + DEVELOPER_CACHE_TTL_MS };
adminPlayersCache = { playerIds, expiresAt: now + ADMIN_CACHE_TTL_MS };
return playerIds;
};
@@ -71,8 +71,8 @@ const fireAlert = async (candidate: AlertCandidate) => {
value: candidate.value,
});
const developers = await getDeveloperPlayerIds();
for (const playerId of developers) {
const admins = await getAdminPlayerIds();
for (const playerId of admins) {
try {
await sendPushToPlayer(playerId, {
title: candidate.title,
+46 -42
View File
@@ -572,7 +572,7 @@ msgstr "Ø Sieg-Marge"
msgid "Award Badge"
msgstr "Abzeichen verleihen"
#: src/features/admin/components/dashboard/tools-list.tsx:63
#: src/features/admin/components/dashboard/tools-list.tsx:60
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:94
#: src/features/admin/components/dashboard/tools-list.tsx:91
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:40
#: src/features/admin/components/dashboard/tools-list.tsx:39
msgid "Badge recalculation is already running"
msgstr "Abzeichen-Neuberechnung läuft bereits"
#: src/features/admin/components/dashboard/tools-list.tsx:39
#: src/features/admin/components/dashboard/tools-list.tsx:38
msgid "Badge recalculation started in the background"
msgstr "Abzeichen-Neuberechnung im Hintergrund gestartet"
@@ -645,7 +645,7 @@ msgid "Bracket"
msgstr "Turnierbaum"
#: src/app/routes/_authed/admin/preview.tsx:13
#: src/features/admin/components/dashboard/tools-list.tsx:98
#: src/features/admin/components/dashboard/tools-list.tsx:102
msgid "Bracket Preview"
msgstr "Turnierbaum-Vorschau"
@@ -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:63
#: src/app/routes/_authed/admin/telemetry.tsx:57
#: 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:43
#: src/features/admin/components/dashboard/tools-list.tsx:42
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:142
#: src/app/routes/api/tournaments/upload-logo.ts:140
#: src/app/routes/api/teams/upload-logo.ts:141
#: src/app/routes/api/tournaments/upload-logo.ts:139
#: 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:81
#: src/app/routes/api/tournaments/upload-logo.ts:82
#: src/app/routes/api/teams/upload-logo.ts:80
#: src/app/routes/api/tournaments/upload-logo.ts:81
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:57
#: src/app/routes/_authed/admin/telemetry.tsx:51
msgid "Functions"
msgstr "Funktionen"
@@ -1335,11 +1335,11 @@ msgstr "Hast du schon mal mitgespielt?"
#: src/app/routes/_authed/stats.tsx:56
#: src/features/bracket/components/match-dock.tsx:174
#: src/features/bracket/components/match-dock.tsx:203
#: src/features/bracket/components/match-dock.tsx:224
#: src/features/matches/components/match-card.tsx:89
#: src/features/matches/components/match-card.tsx:214
#: src/features/matches/components/match-card.tsx:233
#: src/features/players/components/league-head-to-head.tsx:198
#: src/features/players/components/profile/header.tsx:79
#: src/features/players/components/profile/header.tsx:97
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:67
#: src/app/routes/api/tournaments/upload-logo.ts:68
#: src/app/routes/api/teams/upload-logo.ts:66
#: src/app/routes/api/tournaments/upload-logo.ts:67
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:35
#: src/app/routes/api/tournaments/upload-logo.ts:36
#: src/app/routes/api/teams/upload-logo.ts:34
#: src/app/routes/api/tournaments/upload-logo.ts:35
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:82
#: src/features/admin/components/dashboard/tools-list.tsx:79
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:48
#: src/app/routes/api/tournaments/upload-logo.ts:49
#: src/app/routes/api/teams/upload-logo.ts:47
#: src/app/routes/api/tournaments/upload-logo.ts:48
msgid "Logo file is required"
msgstr "Logo-Datei ist erforderlich"
@@ -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:51
#: src/features/admin/components/dashboard/tools-list.tsx:50
msgid "Manage Tournaments"
msgstr "Turniere verwalten"
@@ -1621,7 +1621,7 @@ msgstr "Match gestartet!"
msgid "Matches"
msgstr "Matches"
#: src/features/matches/components/match-list.tsx:34
#: src/features/matches/components/match-list.tsx:33
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:253
#: src/features/predictions/components/prediction-editor.tsx:252
msgid "Matchup"
msgstr "Matchup"
#: src/features/predictions/components/prediction-editor.tsx:230
#: src/features/predictions/components/prediction-editor.tsx:229
msgid "Matchup details"
msgstr "Matchup-Details"
@@ -1646,7 +1646,7 @@ msgstr "MAU"
msgid "Me"
msgstr "Ich"
#: src/features/admin/components/dashboard/tools-list.tsx:68
#: src/features/admin/components/dashboard/tools-list.tsx:65
msgid "Migrate Badge Progress"
msgstr "Abzeichen-Fortschritt migrieren"
@@ -1712,7 +1712,7 @@ msgstr "Neuer Client-Fehler"
msgid "New Player"
msgstr "Neuer Spieler"
#: src/features/predictions/components/prediction-editor.tsx:212
#: src/features/predictions/components/prediction-editor.tsx:211
msgid "Next match"
msgstr "Nächstes Match"
@@ -1908,6 +1908,10 @@ 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"
@@ -2043,7 +2047,7 @@ msgstr "Spieler erfolgreich entfernt"
msgid "Player Stats"
msgstr "Spielerstatistiken"
#: src/app/routes/_authed/admin/telemetry.tsx:54
#: src/app/routes/_authed/admin/telemetry.tsx:48
#: src/features/teams/components/team-profile/index.tsx:29
msgid "Players"
msgstr "Spieler"
@@ -2079,7 +2083,7 @@ msgstr "Tipps"
msgid "Predictions are open"
msgstr "Tipps sind offen"
#: src/features/predictions/components/prediction-editor.tsx:202
#: src/features/predictions/components/prediction-editor.tsx:201
msgid "Previous match"
msgstr "Vorheriges Spiel"
@@ -2091,7 +2095,7 @@ msgstr "Vorheriger Titel"
msgid "Profile"
msgstr "Profil"
#: src/features/badges/components/badge-showcase.tsx:302
#: src/features/badges/components/badge-showcase.tsx:301
msgid "Progress "
msgstr "Fortschritt "
@@ -2118,7 +2122,7 @@ msgstr "Reaktionen"
#. placeholder {0}: migrationStatus.playersProcessed
#. placeholder {1}: migrationStatus.totalPlayers
#: src/features/admin/components/dashboard/tools-list.tsx:75
#: src/features/admin/components/dashboard/tools-list.tsx:72
msgid "Recalculating badges in the background ({0}/{1})"
msgstr "Abzeichen werden im Hintergrund neu berechnet ({0}/{1})"
@@ -2362,7 +2366,7 @@ msgstr "Serverfunktionen"
msgid "serverFn name"
msgstr "serverFn-Name"
#: src/app/routes/_authed/admin/telemetry.tsx:60
#: src/app/routes/_authed/admin/telemetry.tsx:54
#: src/features/telemetry/components/usage-screen.tsx:212
msgid "Sessions"
msgstr "Sitzungen"
@@ -2516,7 +2520,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:244
#: src/features/predictions/components/prediction-editor.tsx:243
#: src/features/tournaments/components/edit-rules.tsx:39
msgid "Submit"
msgstr "Absenden"
@@ -2577,7 +2581,7 @@ msgstr "Team-Details"
msgid "Team enrolled successfully!"
msgstr "Team erfolgreich angemeldet!"
#: src/app/routes/api/teams/upload-logo.ts:94
#: src/app/routes/api/teams/upload-logo.ts:93
#: src/features/teams/components/team-profile/index.tsx:21
msgid "Team not found"
msgstr "Team nicht gefunden"
@@ -2611,8 +2615,8 @@ msgstr "Teams"
msgid "Teams created successfully!"
msgstr "Teams erfolgreich erstellt!"
#: src/app/routes/_authed/admin/telemetry.tsx:26
#: src/features/admin/components/dashboard/tools-list.tsx:57
#: src/app/routes/_authed/admin/telemetry.tsx:20
#: src/features/admin/components/dashboard/tools-list.tsx:55
msgid "Telemetry"
msgstr "Telemetrie"
@@ -2726,7 +2730,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:95
#: src/app/routes/api/tournaments/upload-logo.ts:94
msgid "Tournament not found"
msgstr "Turnier nicht gefunden"
@@ -2780,8 +2784,8 @@ msgstr "Team abmelden"
msgid "Unknown error"
msgstr "Unbekannter Fehler"
#: src/app/routes/api/teams/upload-logo.ts:143
#: src/app/routes/api/tournaments/upload-logo.ts:141
#: src/app/routes/api/teams/upload-logo.ts:142
#: src/app/routes/api/tournaments/upload-logo.ts:140
msgid "Unknown error occurred"
msgstr "Unbekannter Fehler aufgetreten"
@@ -2806,7 +2810,7 @@ msgstr "Bevorstehend"
msgid "Update Match"
msgstr "Match aktualisieren"
#: src/features/players/components/profile/header.tsx:74
#: src/features/players/components/profile/header.tsx:92
msgid "Update Name"
msgstr "Namen aktualisieren"
@@ -2820,7 +2824,7 @@ msgstr "Team aktualisieren"
msgid "Update Tournament"
msgstr "Turnier aktualisieren"
#: src/app/routes/_authed/admin/telemetry.tsx:51
#: src/app/routes/_authed/admin/telemetry.tsx:45
msgid "Usage"
msgstr "Nutzung"
+46 -42
View File
@@ -572,7 +572,7 @@ msgstr "Avg Win Margin"
msgid "Award Badge"
msgstr "Award Badge"
#: src/features/admin/components/dashboard/tools-list.tsx:63
#: src/features/admin/components/dashboard/tools-list.tsx:60
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:94
#: src/features/admin/components/dashboard/tools-list.tsx:91
msgid "Badge recalculation failed. Check the server logs."
msgstr "Badge recalculation failed. Check the server logs."
#: src/features/admin/components/dashboard/tools-list.tsx:40
#: src/features/admin/components/dashboard/tools-list.tsx:39
msgid "Badge recalculation is already running"
msgstr "Badge recalculation is already running"
#: src/features/admin/components/dashboard/tools-list.tsx:39
#: src/features/admin/components/dashboard/tools-list.tsx:38
msgid "Badge recalculation started in the background"
msgstr "Badge recalculation started in the background"
@@ -645,7 +645,7 @@ msgid "Bracket"
msgstr "Bracket"
#: src/app/routes/_authed/admin/preview.tsx:13
#: src/features/admin/components/dashboard/tools-list.tsx:98
#: src/features/admin/components/dashboard/tools-list.tsx:102
msgid "Bracket Preview"
msgstr "Bracket Preview"
@@ -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:63
#: src/app/routes/_authed/admin/telemetry.tsx:57
#: 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:43
#: src/features/admin/components/dashboard/tools-list.tsx:42
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:142
#: src/app/routes/api/tournaments/upload-logo.ts:140
#: src/app/routes/api/teams/upload-logo.ts:141
#: src/app/routes/api/tournaments/upload-logo.ts:139
#: 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:81
#: src/app/routes/api/tournaments/upload-logo.ts:82
#: src/app/routes/api/teams/upload-logo.ts:80
#: src/app/routes/api/tournaments/upload-logo.ts:81
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:57
#: src/app/routes/_authed/admin/telemetry.tsx:51
msgid "Functions"
msgstr "Functions"
@@ -1335,11 +1335,11 @@ msgstr "Have you played before?"
#: src/app/routes/_authed/stats.tsx:56
#: src/features/bracket/components/match-dock.tsx:174
#: src/features/bracket/components/match-dock.tsx:203
#: src/features/bracket/components/match-dock.tsx:224
#: src/features/matches/components/match-card.tsx:89
#: src/features/matches/components/match-card.tsx:214
#: src/features/matches/components/match-card.tsx:233
#: src/features/players/components/league-head-to-head.tsx:198
#: src/features/players/components/profile/header.tsx:79
#: src/features/players/components/profile/header.tsx:97
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:67
#: src/app/routes/api/tournaments/upload-logo.ts:68
#: src/app/routes/api/teams/upload-logo.ts:66
#: src/app/routes/api/tournaments/upload-logo.ts:67
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:35
#: src/app/routes/api/tournaments/upload-logo.ts:36
#: src/app/routes/api/teams/upload-logo.ts:34
#: src/app/routes/api/tournaments/upload-logo.ts:35
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:82
#: src/features/admin/components/dashboard/tools-list.tsx:79
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:48
#: src/app/routes/api/tournaments/upload-logo.ts:49
#: src/app/routes/api/teams/upload-logo.ts:47
#: src/app/routes/api/tournaments/upload-logo.ts:48
msgid "Logo file is required"
msgstr "Logo file is required"
@@ -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:51
#: src/features/admin/components/dashboard/tools-list.tsx:50
msgid "Manage Tournaments"
msgstr "Manage Tournaments"
@@ -1621,7 +1621,7 @@ msgstr "Match started!"
msgid "Matches"
msgstr "Matches"
#: src/features/matches/components/match-list.tsx:34
#: src/features/matches/components/match-list.tsx:33
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:253
#: src/features/predictions/components/prediction-editor.tsx:252
msgid "Matchup"
msgstr "Matchup"
#: src/features/predictions/components/prediction-editor.tsx:230
#: src/features/predictions/components/prediction-editor.tsx:229
msgid "Matchup details"
msgstr "Matchup details"
@@ -1646,7 +1646,7 @@ msgstr "MAU"
msgid "Me"
msgstr "Me"
#: src/features/admin/components/dashboard/tools-list.tsx:68
#: src/features/admin/components/dashboard/tools-list.tsx:65
msgid "Migrate Badge Progress"
msgstr "Migrate Badge Progress"
@@ -1712,7 +1712,7 @@ msgstr "New client error"
msgid "New Player"
msgstr "New Player"
#: src/features/predictions/components/prediction-editor.tsx:212
#: src/features/predictions/components/prediction-editor.tsx:211
msgid "Next match"
msgstr "Next match"
@@ -1908,6 +1908,10 @@ 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"
@@ -2043,7 +2047,7 @@ msgstr "Player removed successfully"
msgid "Player Stats"
msgstr "Player Stats"
#: src/app/routes/_authed/admin/telemetry.tsx:54
#: src/app/routes/_authed/admin/telemetry.tsx:48
#: src/features/teams/components/team-profile/index.tsx:29
msgid "Players"
msgstr "Players"
@@ -2079,7 +2083,7 @@ msgstr "Predictions"
msgid "Predictions are open"
msgstr "Predictions are open"
#: src/features/predictions/components/prediction-editor.tsx:202
#: src/features/predictions/components/prediction-editor.tsx:201
msgid "Previous match"
msgstr "Previous match"
@@ -2091,7 +2095,7 @@ msgstr "Previous track"
msgid "Profile"
msgstr "Profile"
#: src/features/badges/components/badge-showcase.tsx:302
#: src/features/badges/components/badge-showcase.tsx:301
msgid "Progress "
msgstr "Progress "
@@ -2118,7 +2122,7 @@ msgstr "Reactions"
#. placeholder {0}: migrationStatus.playersProcessed
#. placeholder {1}: migrationStatus.totalPlayers
#: src/features/admin/components/dashboard/tools-list.tsx:75
#: src/features/admin/components/dashboard/tools-list.tsx:72
msgid "Recalculating badges in the background ({0}/{1})"
msgstr "Recalculating badges in the background ({0}/{1})"
@@ -2362,7 +2366,7 @@ msgstr "Server functions"
msgid "serverFn name"
msgstr "serverFn name"
#: src/app/routes/_authed/admin/telemetry.tsx:60
#: src/app/routes/_authed/admin/telemetry.tsx:54
#: src/features/telemetry/components/usage-screen.tsx:212
msgid "Sessions"
msgstr "Sessions"
@@ -2516,7 +2520,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:244
#: src/features/predictions/components/prediction-editor.tsx:243
#: src/features/tournaments/components/edit-rules.tsx:39
msgid "Submit"
msgstr "Submit"
@@ -2577,7 +2581,7 @@ msgstr "Team Details"
msgid "Team enrolled successfully!"
msgstr "Team enrolled successfully!"
#: src/app/routes/api/teams/upload-logo.ts:94
#: src/app/routes/api/teams/upload-logo.ts:93
#: src/features/teams/components/team-profile/index.tsx:21
msgid "Team not found"
msgstr "Team not found"
@@ -2611,8 +2615,8 @@ msgstr "Teams"
msgid "Teams created successfully!"
msgstr "Teams created successfully!"
#: src/app/routes/_authed/admin/telemetry.tsx:26
#: src/features/admin/components/dashboard/tools-list.tsx:57
#: src/app/routes/_authed/admin/telemetry.tsx:20
#: src/features/admin/components/dashboard/tools-list.tsx:55
msgid "Telemetry"
msgstr "Telemetry"
@@ -2726,7 +2730,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:95
#: src/app/routes/api/tournaments/upload-logo.ts:94
msgid "Tournament not found"
msgstr "Tournament not found"
@@ -2780,8 +2784,8 @@ msgstr "Unenroll Team"
msgid "Unknown error"
msgstr "Unknown error"
#: src/app/routes/api/teams/upload-logo.ts:143
#: src/app/routes/api/tournaments/upload-logo.ts:141
#: src/app/routes/api/teams/upload-logo.ts:142
#: src/app/routes/api/tournaments/upload-logo.ts:140
msgid "Unknown error occurred"
msgstr "Unknown error occurred"
@@ -2806,7 +2810,7 @@ msgstr "Upcoming"
msgid "Update Match"
msgstr "Update Match"
#: src/features/players/components/profile/header.tsx:74
#: src/features/players/components/profile/header.tsx:92
msgid "Update Name"
msgstr "Update Name"
@@ -2820,7 +2824,7 @@ msgstr "Update Team"
msgid "Update Tournament"
msgstr "Update Tournament"
#: src/app/routes/_authed/admin/telemetry.tsx:51
#: src/app/routes/_authed/admin/telemetry.tsx:45
msgid "Usage"
msgstr "Usage"
+46 -42
View File
@@ -572,7 +572,7 @@ msgstr "Prom. Margen de Victoria"
msgid "Award Badge"
msgstr "Otorgar insignia"
#: src/features/admin/components/dashboard/tools-list.tsx:63
#: src/features/admin/components/dashboard/tools-list.tsx:60
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:94
#: src/features/admin/components/dashboard/tools-list.tsx:91
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:40
#: src/features/admin/components/dashboard/tools-list.tsx:39
msgid "Badge recalculation is already running"
msgstr "El recálculo de insignias ya está en curso"
#: src/features/admin/components/dashboard/tools-list.tsx:39
#: src/features/admin/components/dashboard/tools-list.tsx:38
msgid "Badge recalculation started in the background"
msgstr "Recálculo de insignias iniciado en segundo plano"
@@ -645,7 +645,7 @@ msgid "Bracket"
msgstr "Bracket"
#: src/app/routes/_authed/admin/preview.tsx:13
#: src/features/admin/components/dashboard/tools-list.tsx:98
#: src/features/admin/components/dashboard/tools-list.tsx:102
msgid "Bracket Preview"
msgstr "Vista previa del bracket"
@@ -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:63
#: src/app/routes/_authed/admin/telemetry.tsx:57
#: 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:43
#: src/features/admin/components/dashboard/tools-list.tsx:42
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:142
#: src/app/routes/api/tournaments/upload-logo.ts:140
#: src/app/routes/api/teams/upload-logo.ts:141
#: src/app/routes/api/tournaments/upload-logo.ts:139
#: 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:81
#: src/app/routes/api/tournaments/upload-logo.ts:82
#: src/app/routes/api/teams/upload-logo.ts:80
#: src/app/routes/api/tournaments/upload-logo.ts:81
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:57
#: src/app/routes/_authed/admin/telemetry.tsx:51
msgid "Functions"
msgstr "Funciones"
@@ -1335,11 +1335,11 @@ msgstr "¿Ya has jugado antes?"
#: src/app/routes/_authed/stats.tsx:56
#: src/features/bracket/components/match-dock.tsx:174
#: src/features/bracket/components/match-dock.tsx:203
#: src/features/bracket/components/match-dock.tsx:224
#: src/features/matches/components/match-card.tsx:89
#: src/features/matches/components/match-card.tsx:214
#: src/features/matches/components/match-card.tsx:233
#: src/features/players/components/league-head-to-head.tsx:198
#: src/features/players/components/profile/header.tsx:79
#: src/features/players/components/profile/header.tsx:97
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:67
#: src/app/routes/api/tournaments/upload-logo.ts:68
#: src/app/routes/api/teams/upload-logo.ts:66
#: src/app/routes/api/tournaments/upload-logo.ts:67
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:35
#: src/app/routes/api/tournaments/upload-logo.ts:36
#: src/app/routes/api/teams/upload-logo.ts:34
#: src/app/routes/api/tournaments/upload-logo.ts:35
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:82
#: src/features/admin/components/dashboard/tools-list.tsx:79
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:48
#: src/app/routes/api/tournaments/upload-logo.ts:49
#: src/app/routes/api/teams/upload-logo.ts:47
#: src/app/routes/api/tournaments/upload-logo.ts:48
msgid "Logo file is required"
msgstr "El archivo del logo es obligatorio"
@@ -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:51
#: src/features/admin/components/dashboard/tools-list.tsx:50
msgid "Manage Tournaments"
msgstr "Administrar torneos"
@@ -1621,7 +1621,7 @@ msgstr "¡Empezó el partido!"
msgid "Matches"
msgstr "Partidos"
#: src/features/matches/components/match-list.tsx:34
#: src/features/matches/components/match-list.tsx:33
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:253
#: src/features/predictions/components/prediction-editor.tsx:252
msgid "Matchup"
msgstr "Enfrentamiento"
#: src/features/predictions/components/prediction-editor.tsx:230
#: src/features/predictions/components/prediction-editor.tsx:229
msgid "Matchup details"
msgstr "Detalles del enfrentamiento"
@@ -1646,7 +1646,7 @@ msgstr "MAU"
msgid "Me"
msgstr "Yo"
#: src/features/admin/components/dashboard/tools-list.tsx:68
#: src/features/admin/components/dashboard/tools-list.tsx:65
msgid "Migrate Badge Progress"
msgstr "Migrar progreso de insignias"
@@ -1712,7 +1712,7 @@ msgstr "Nuevo error de cliente"
msgid "New Player"
msgstr "Nuevo jugador"
#: src/features/predictions/components/prediction-editor.tsx:212
#: src/features/predictions/components/prediction-editor.tsx:211
msgid "Next match"
msgstr "Siguiente partido"
@@ -1908,6 +1908,10 @@ 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"
@@ -2043,7 +2047,7 @@ msgstr "Jugador eliminado con éxito"
msgid "Player Stats"
msgstr "Estadísticas de jugador"
#: src/app/routes/_authed/admin/telemetry.tsx:54
#: src/app/routes/_authed/admin/telemetry.tsx:48
#: src/features/teams/components/team-profile/index.tsx:29
msgid "Players"
msgstr "Jugadores"
@@ -2079,7 +2083,7 @@ msgstr "Predicciones"
msgid "Predictions are open"
msgstr "Las predicciones están abiertas"
#: src/features/predictions/components/prediction-editor.tsx:202
#: src/features/predictions/components/prediction-editor.tsx:201
msgid "Previous match"
msgstr "Partido anterior"
@@ -2091,7 +2095,7 @@ msgstr "Pista anterior"
msgid "Profile"
msgstr "Perfil"
#: src/features/badges/components/badge-showcase.tsx:302
#: src/features/badges/components/badge-showcase.tsx:301
msgid "Progress "
msgstr "Progreso "
@@ -2118,7 +2122,7 @@ msgstr "Reacciones"
#. placeholder {0}: migrationStatus.playersProcessed
#. placeholder {1}: migrationStatus.totalPlayers
#: src/features/admin/components/dashboard/tools-list.tsx:75
#: src/features/admin/components/dashboard/tools-list.tsx:72
msgid "Recalculating badges in the background ({0}/{1})"
msgstr "Recalculando insignias en segundo plano ({0}/{1})"
@@ -2362,7 +2366,7 @@ msgstr "Funciones del servidor"
msgid "serverFn name"
msgstr "nombre de serverFn"
#: src/app/routes/_authed/admin/telemetry.tsx:60
#: src/app/routes/_authed/admin/telemetry.tsx:54
#: src/features/telemetry/components/usage-screen.tsx:212
msgid "Sessions"
msgstr "Sesiones"
@@ -2516,7 +2520,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:244
#: src/features/predictions/components/prediction-editor.tsx:243
#: src/features/tournaments/components/edit-rules.tsx:39
msgid "Submit"
msgstr "Enviar"
@@ -2577,7 +2581,7 @@ msgstr "Detalles del equipo"
msgid "Team enrolled successfully!"
msgstr "¡Equipo inscrito exitosamente!"
#: src/app/routes/api/teams/upload-logo.ts:94
#: src/app/routes/api/teams/upload-logo.ts:93
#: src/features/teams/components/team-profile/index.tsx:21
msgid "Team not found"
msgstr "Equipo no encontrado"
@@ -2611,8 +2615,8 @@ msgstr "Equipos"
msgid "Teams created successfully!"
msgstr "¡Equipos creados exitosamente!"
#: src/app/routes/_authed/admin/telemetry.tsx:26
#: src/features/admin/components/dashboard/tools-list.tsx:57
#: src/app/routes/_authed/admin/telemetry.tsx:20
#: src/features/admin/components/dashboard/tools-list.tsx:55
msgid "Telemetry"
msgstr "Telemetría"
@@ -2726,7 +2730,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:95
#: src/app/routes/api/tournaments/upload-logo.ts:94
msgid "Tournament not found"
msgstr "Torneo no encontrado"
@@ -2780,8 +2784,8 @@ msgstr "Cancelar inscripción del equipo"
msgid "Unknown error"
msgstr "Error desconocido"
#: src/app/routes/api/teams/upload-logo.ts:143
#: src/app/routes/api/tournaments/upload-logo.ts:141
#: src/app/routes/api/teams/upload-logo.ts:142
#: src/app/routes/api/tournaments/upload-logo.ts:140
msgid "Unknown error occurred"
msgstr "Ocurrió un error desconocido"
@@ -2806,7 +2810,7 @@ msgstr "Próximos"
msgid "Update Match"
msgstr "Actualizar partido"
#: src/features/players/components/profile/header.tsx:74
#: src/features/players/components/profile/header.tsx:92
msgid "Update Name"
msgstr "Actualizar nombre"
@@ -2820,7 +2824,7 @@ msgstr "Actualizar equipo"
msgid "Update Tournament"
msgstr "Actualizar torneo"
#: src/app/routes/_authed/admin/telemetry.tsx:51
#: src/app/routes/_authed/admin/telemetry.tsx:45
msgid "Usage"
msgstr "Uso"
+46 -42
View File
@@ -572,7 +572,7 @@ msgstr "平均勝差"
msgid "Award Badge"
msgstr "バッジを付与"
#: src/features/admin/components/dashboard/tools-list.tsx:63
#: src/features/admin/components/dashboard/tools-list.tsx:60
msgid "Award Badges"
msgstr "バッジを付与"
@@ -606,15 +606,15 @@ msgstr "戻る"
msgid "Badge awarded to {playerName}"
msgstr "{playerName}にバッジを付与しました"
#: src/features/admin/components/dashboard/tools-list.tsx:94
#: src/features/admin/components/dashboard/tools-list.tsx:91
msgid "Badge recalculation failed. Check the server logs."
msgstr "バッジの再計算に失敗しました。サーバーログを確認してください。"
#: src/features/admin/components/dashboard/tools-list.tsx:40
#: src/features/admin/components/dashboard/tools-list.tsx:39
msgid "Badge recalculation is already running"
msgstr "バッジの再計算はすでに実行中です"
#: src/features/admin/components/dashboard/tools-list.tsx:39
#: src/features/admin/components/dashboard/tools-list.tsx:38
msgid "Badge recalculation started in the background"
msgstr "バックグラウンドでバッジの再計算を開始しました"
@@ -645,7 +645,7 @@ msgid "Bracket"
msgstr "ブラケット"
#: src/app/routes/_authed/admin/preview.tsx:13
#: src/features/admin/components/dashboard/tools-list.tsx:98
#: src/features/admin/components/dashboard/tools-list.tsx:102
msgid "Bracket Preview"
msgstr "ブラケットプレビュー"
@@ -1118,7 +1118,7 @@ msgstr "エラーの急増や失敗している関数をこのアカウントに
msgid "Error: {errorMessage}"
msgstr "エラー: {errorMessage}"
#: src/app/routes/_authed/admin/telemetry.tsx:63
#: src/app/routes/_authed/admin/telemetry.tsx:57
#: 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:43
#: src/features/admin/components/dashboard/tools-list.tsx:42
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:142
#: src/app/routes/api/tournaments/upload-logo.ts:140
#: src/app/routes/api/teams/upload-logo.ts:141
#: src/app/routes/api/tournaments/upload-logo.ts:139
#: 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:81
#: src/app/routes/api/tournaments/upload-logo.ts:82
#: src/app/routes/api/teams/upload-logo.ts:80
#: src/app/routes/api/tournaments/upload-logo.ts:81
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:57
#: src/app/routes/_authed/admin/telemetry.tsx:51
msgid "Functions"
msgstr "関数"
@@ -1335,11 +1335,11 @@ msgstr "参加したことはありますか?"
#: src/app/routes/_authed/stats.tsx:56
#: src/features/bracket/components/match-dock.tsx:174
#: src/features/bracket/components/match-dock.tsx:203
#: src/features/bracket/components/match-dock.tsx:224
#: src/features/matches/components/match-card.tsx:89
#: src/features/matches/components/match-card.tsx:214
#: src/features/matches/components/match-card.tsx:233
#: src/features/players/components/league-head-to-head.tsx:198
#: src/features/players/components/profile/header.tsx:79
#: src/features/players/components/profile/header.tsx:97
msgid "Head to Head"
msgstr "対戦成績"
@@ -1401,8 +1401,8 @@ msgstr "コードが正しくありません"
msgid "Invalid action"
msgstr "無効な操作です"
#: src/app/routes/api/teams/upload-logo.ts:67
#: src/app/routes/api/tournaments/upload-logo.ts:68
#: src/app/routes/api/teams/upload-logo.ts:66
#: src/app/routes/api/tournaments/upload-logo.ts:67
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:35
#: src/app/routes/api/tournaments/upload-logo.ts:36
#: src/app/routes/api/teams/upload-logo.ts:34
#: src/app/routes/api/tournaments/upload-logo.ts:35
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:82
#: src/features/admin/components/dashboard/tools-list.tsx:79
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:48
#: src/app/routes/api/tournaments/upload-logo.ts:49
#: src/app/routes/api/teams/upload-logo.ts:47
#: src/app/routes/api/tournaments/upload-logo.ts:48
msgid "Logo file is required"
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:51
#: src/features/admin/components/dashboard/tools-list.tsx:50
msgid "Manage Tournaments"
msgstr "トーナメント管理"
@@ -1621,7 +1621,7 @@ msgstr "試合開始!"
msgid "Matches"
msgstr "試合"
#: src/features/matches/components/match-list.tsx:34
#: src/features/matches/components/match-list.tsx:33
msgid "Matches for regionals are unordered"
msgstr "地区予選の試合は順不同です"
@@ -1630,11 +1630,11 @@ msgstr "地区予選の試合は順不同です"
msgid "Matches Played"
msgstr "試合数"
#: src/features/predictions/components/prediction-editor.tsx:253
#: src/features/predictions/components/prediction-editor.tsx:252
msgid "Matchup"
msgstr "対戦カード"
#: src/features/predictions/components/prediction-editor.tsx:230
#: src/features/predictions/components/prediction-editor.tsx:229
msgid "Matchup details"
msgstr "対戦カードの詳細"
@@ -1646,7 +1646,7 @@ msgstr "MAU"
msgid "Me"
msgstr "自分"
#: src/features/admin/components/dashboard/tools-list.tsx:68
#: src/features/admin/components/dashboard/tools-list.tsx:65
msgid "Migrate Badge Progress"
msgstr "バッジ進捗を移行"
@@ -1712,7 +1712,7 @@ msgstr "新しいクライアントエラー"
msgid "New Player"
msgstr "新しいプレイヤー"
#: src/features/predictions/components/prediction-editor.tsx:212
#: src/features/predictions/components/prediction-editor.tsx:211
msgid "Next match"
msgstr "次の試合"
@@ -1908,6 +1908,10 @@ 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 "対戦相手"
@@ -2043,7 +2047,7 @@ msgstr "プレイヤーを削除しました"
msgid "Player Stats"
msgstr "プレイヤースタッツ"
#: src/app/routes/_authed/admin/telemetry.tsx:54
#: src/app/routes/_authed/admin/telemetry.tsx:48
#: src/features/teams/components/team-profile/index.tsx:29
msgid "Players"
msgstr "プレイヤー"
@@ -2079,7 +2083,7 @@ msgstr "予想"
msgid "Predictions are open"
msgstr "予想受付中"
#: src/features/predictions/components/prediction-editor.tsx:202
#: src/features/predictions/components/prediction-editor.tsx:201
msgid "Previous match"
msgstr "前の試合"
@@ -2091,7 +2095,7 @@ msgstr "前のトラック"
msgid "Profile"
msgstr "プロフィール"
#: src/features/badges/components/badge-showcase.tsx:302
#: src/features/badges/components/badge-showcase.tsx:301
msgid "Progress "
msgstr "進行状況 "
@@ -2118,7 +2122,7 @@ msgstr "リアクション"
#. placeholder {0}: migrationStatus.playersProcessed
#. placeholder {1}: migrationStatus.totalPlayers
#: src/features/admin/components/dashboard/tools-list.tsx:75
#: src/features/admin/components/dashboard/tools-list.tsx:72
msgid "Recalculating badges in the background ({0}/{1})"
msgstr "バックグラウンドでバッジを再計算中({0}/{1})"
@@ -2362,7 +2366,7 @@ msgstr "サーバー関数"
msgid "serverFn name"
msgstr "serverFn名"
#: src/app/routes/_authed/admin/telemetry.tsx:60
#: src/app/routes/_authed/admin/telemetry.tsx:54
#: src/features/telemetry/components/usage-screen.tsx:212
msgid "Sessions"
msgstr "セッション数"
@@ -2516,7 +2520,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:244
#: src/features/predictions/components/prediction-editor.tsx:243
#: src/features/tournaments/components/edit-rules.tsx:39
msgid "Submit"
msgstr "送信"
@@ -2577,7 +2581,7 @@ msgstr "チーム詳細"
msgid "Team enrolled successfully!"
msgstr "チームを登録しました!"
#: src/app/routes/api/teams/upload-logo.ts:94
#: src/app/routes/api/teams/upload-logo.ts:93
#: src/features/teams/components/team-profile/index.tsx:21
msgid "Team not found"
msgstr "チームが見つかりません"
@@ -2611,8 +2615,8 @@ msgstr "チーム"
msgid "Teams created successfully!"
msgstr "チームを作成しました!"
#: src/app/routes/_authed/admin/telemetry.tsx:26
#: src/features/admin/components/dashboard/tools-list.tsx:57
#: src/app/routes/_authed/admin/telemetry.tsx:20
#: src/features/admin/components/dashboard/tools-list.tsx:55
msgid "Telemetry"
msgstr "テレメトリー"
@@ -2726,7 +2730,7 @@ msgstr "トーナメントを作成しましたが、ロゴのアップロード
msgid "Tournament created successfully!"
msgstr "トーナメントを作成しました!"
#: src/app/routes/api/tournaments/upload-logo.ts:95
#: src/app/routes/api/tournaments/upload-logo.ts:94
msgid "Tournament not found"
msgstr "トーナメントが見つかりません"
@@ -2780,8 +2784,8 @@ msgstr "チームを登録解除"
msgid "Unknown error"
msgstr "不明なエラー"
#: src/app/routes/api/teams/upload-logo.ts:143
#: src/app/routes/api/tournaments/upload-logo.ts:141
#: src/app/routes/api/teams/upload-logo.ts:142
#: src/app/routes/api/tournaments/upload-logo.ts:140
msgid "Unknown error occurred"
msgstr "不明なエラーが発生しました"
@@ -2806,7 +2810,7 @@ msgstr "開催予定"
msgid "Update Match"
msgstr "試合を更新"
#: src/features/players/components/profile/header.tsx:74
#: src/features/players/components/profile/header.tsx:92
msgid "Update Name"
msgstr "名前を更新"
@@ -2820,7 +2824,7 @@ msgstr "チームを更新"
msgid "Update Tournament"
msgstr "トーナメントを更新"
#: src/app/routes/_authed/admin/telemetry.tsx:51
#: src/app/routes/_authed/admin/telemetry.tsx:45
msgid "Usage"
msgstr "利用状況"
+4 -3
View File
@@ -178,14 +178,15 @@
/* 3. Fixed sky backdrop with static bubbles (below all content) */
/* ---------------------------------------------------------------- */
[data-theme="aero"] .flxn-theme-overlay {
[data-theme="aero"] body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
}
[data-theme="aero"][data-mantine-color-scheme="light"] .flxn-theme-overlay {
[data-theme="aero"][data-mantine-color-scheme="light"] body::before {
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%),
@@ -197,7 +198,7 @@
linear-gradient(180deg, #a5daf6 0%, #c9e9fa 32%, #e9f6fe 60%, #ffffff 100%);
}
[data-theme="aero"][data-mantine-color-scheme="dark"] .flxn-theme-overlay {
[data-theme="aero"][data-mantine-color-scheme="dark"] body::before {
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
View File
@@ -4,10 +4,6 @@
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);
+4 -3
View File
@@ -203,7 +203,8 @@
/* ---------- 4. Backdrop — studio key light + clay grain ---------- */
[data-theme="clay"] .flxn-theme-overlay {
[data-theme="clay"] body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1; /* behind content — never tints text or surfaces */
@@ -211,7 +212,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"] .flxn-theme-overlay {
[data-theme="clay"][data-mantine-color-scheme="light"] body::before {
background-image: radial-gradient(
120% 70% at 50% 0%,
rgba(255, 252, 244, 0.65),
@@ -220,7 +221,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"] .flxn-theme-overlay {
[data-theme="clay"][data-mantine-color-scheme="dark"] body::before {
background-image: radial-gradient(
110% 65% at 50% 0%,
rgba(255, 208, 158, 0.09),
+2 -1
View File
@@ -363,7 +363,8 @@
/* ------------------------------------------------------------------ */
/* 4. Sunburst fan — faint gold rays from top center */
/* ------------------------------------------------------------------ */
[data-theme="deco"] .flxn-theme-overlay {
[data-theme="deco"] body::before {
content: "";
position: fixed;
inset: 0;
z-index: 9998;
+2 -1
View File
@@ -409,7 +409,8 @@
* 3. Full-page decoration: candlelit vignette (dark scheme only)
* ---------------------------------------------------------------- */
[data-theme="gothic"][data-mantine-color-scheme="dark"] .flxn-theme-overlay {
[data-theme="gothic"][data-mantine-color-scheme="dark"] body::before {
content: "";
position: fixed;
inset: 0;
z-index: 9998;
-58
View File
@@ -67,61 +67,3 @@
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;
}
}
+4 -3
View File
@@ -153,7 +153,8 @@
/* 3. Fixed backdrop (below all content) */
/* ---------------------------------------------------------------- */
[data-theme="ocean"] .flxn-theme-overlay {
[data-theme="ocean"] body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
@@ -162,7 +163,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"] .flxn-theme-overlay {
[data-theme="ocean"][data-mantine-color-scheme="dark"] body::before {
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"),
@@ -174,7 +175,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"] .flxn-theme-overlay {
[data-theme="ocean"][data-mantine-color-scheme="light"] body::before {
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"),
+4 -2
View File
@@ -414,7 +414,8 @@
* 3. Full-page decoration: worn parchment edges / torchlight
* ---------------------------------------------------------------- */
[data-theme="quest"][data-mantine-color-scheme="light"] .flxn-theme-overlay {
[data-theme="quest"][data-mantine-color-scheme="light"] body::before {
content: "";
position: fixed;
inset: 0;
z-index: 9998;
@@ -425,7 +426,8 @@
radial-gradient(55% 38% at 95% 98%, rgba(122, 91, 42, 0.07), transparent 70%);
}
[data-theme="quest"][data-mantine-color-scheme="dark"] .flxn-theme-overlay {
[data-theme="quest"][data-mantine-color-scheme="dark"] body::before {
content: "";
position: fixed;
inset: 0;
z-index: 9998;
+3 -2
View File
@@ -151,7 +151,8 @@
/* 3. Paper grain / chalk dust — one fixed overlay, feTurbulence */
/* ---------------------------------------------------------------- */
[data-theme="sketch"] .flxn-theme-overlay {
[data-theme="sketch"] body::before {
content: "";
position: fixed;
inset: 0;
z-index: 9998;
@@ -162,7 +163,7 @@
opacity: 0.05;
}
[data-theme="sketch"][data-mantine-color-scheme="dark"] .flxn-theme-overlay {
[data-theme="sketch"][data-mantine-color-scheme="dark"] body::before {
mix-blend-mode: screen;
opacity: 0.07;
}
+3 -2
View File
@@ -575,7 +575,8 @@
}
/* 4. Decoration: fixed scanline overlay + faint RGB aperture mask */
[data-theme="terminal"] .flxn-theme-overlay {
[data-theme="terminal"] body::before {
content: "";
position: fixed;
inset: 0;
z-index: 9998;
@@ -588,7 +589,7 @@
}
@media (prefers-reduced-motion: no-preference) {
[data-theme="terminal"] .flxn-theme-overlay {
[data-theme="terminal"] body::before {
animation: terminal-phosphor-drift 9s ease-in-out infinite;
}
@keyframes terminal-phosphor-drift {
+2 -1
View File
@@ -150,7 +150,8 @@
/* ---------- 3. Decoration: static perspective floor-grid hint ---------- */
[data-theme="wireframe"] .flxn-theme-overlay {
[data-theme="wireframe"] body::before {
content: "";
position: fixed;
left: -30vw;
right: -30vw;
+4 -18
View File
@@ -5,7 +5,7 @@ import {
} from "@tanstack/react-start";
import { getRequest } from "@tanstack/react-start/server";
import { Logger } from "@/lib/logger";
import { isAdminRole, isDeveloperRole } from "@/features/core/utils/roles";
import { isAdminRole } from "@/features/core/utils/roles";
import z from "zod";
const logger = new Logger("Middleware");
@@ -70,11 +70,7 @@ export const superTokensFunctionMiddleware = createMiddleware({
}
});
const createRoleFunctionMiddleware = (
hasRole: (roles?: string[] | null) => boolean,
label: string
) =>
createMiddleware({
export const superTokensAdminFunctionMiddleware = createMiddleware({
type: "function",
}).server(async ({ next, serverFnMeta }) => {
const request = getRequest();
@@ -82,11 +78,11 @@ const createRoleFunctionMiddleware = (
try {
const context = await getSessionContext(request, { isServerFunction: true });
if (hasRole(context.roles)) {
if (isAdminRole(context.roles)) {
return next({ context });
}
logger.error(`Unauthorized user in ${label} function.`, {
logger.error("Unauthorized user in admin function.", {
userAuthId: context.userAuthId,
roles: context.roles,
});
@@ -110,16 +106,6 @@ const createRoleFunctionMiddleware = (
}
});
export const superTokensAdminFunctionMiddleware = createRoleFunctionMiddleware(
isAdminRole,
"admin"
);
export const superTokensDeveloperFunctionMiddleware = createRoleFunctionMiddleware(
isDeveloperRole,
"developer"
);
export const setUserMetadata = createServerFn({ method: "POST" })
.validator(
z