Development #26
@@ -31,8 +31,8 @@
|
|||||||
"facehash": "^0.0.7",
|
"facehash": "^0.0.7",
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
"pocketbase": "^0.26.2",
|
"pocketbase": "^0.26.2",
|
||||||
"react": "^19.2.7",
|
"react": "^19.3.0",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.3.0",
|
||||||
"react-imask": "^7.6.1",
|
"react-imask": "^7.6.1",
|
||||||
"redaxios": "^0.5.1",
|
"redaxios": "^0.5.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
@@ -49,8 +49,8 @@
|
|||||||
"@lingui/cli": "^6.5.0",
|
"@lingui/cli": "^6.5.0",
|
||||||
"@lingui/vite-plugin": "^6.5.0",
|
"@lingui/vite-plugin": "^6.5.0",
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.5.4",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.3.0",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.3.0",
|
||||||
"@types/web-push": "^3.6.4",
|
"@types/web-push": "^3.6.4",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"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/node": ["@types/node@22.19.10", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
"@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="],
|
||||||
|
|
||||||
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
"@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=="],
|
"raf-schd": ["raf-schd@4.0.3", "", {}, "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="],
|
||||||
|
|
||||||
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
|
"react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
|
"react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="],
|
||||||
|
|
||||||
"react-imask": ["react-imask@7.6.1", "", { "dependencies": { "imask": "^7.6.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=0.14.0" } }, "sha512-vLNfzcCz62Yzx/GRGh5tiCph9Gbh2cZu+Tz8OiO5it2eNuuhpA0DWhhSlOtVtSJ80+Bx+vFK5De8eQ9AmbkXzA=="],
|
"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=="],
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="],
|
||||||
|
|
||||||
"scmp": ["scmp@2.1.0", "", {}, "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q=="],
|
"scmp": ["scmp@2.1.0", "", {}, "sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q=="],
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -43,8 +43,8 @@
|
|||||||
"facehash": "^0.0.7",
|
"facehash": "^0.0.7",
|
||||||
"framer-motion": "^12.23.12",
|
"framer-motion": "^12.23.12",
|
||||||
"pocketbase": "^0.26.2",
|
"pocketbase": "^0.26.2",
|
||||||
"react": "^19.2.7",
|
"react": "^19.3.0",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.3.0",
|
||||||
"react-imask": "^7.6.1",
|
"react-imask": "^7.6.1",
|
||||||
"redaxios": "^0.5.1",
|
"redaxios": "^0.5.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
@@ -61,8 +61,8 @@
|
|||||||
"@lingui/cli": "^6.5.0",
|
"@lingui/cli": "^6.5.0",
|
||||||
"@lingui/vite-plugin": "^6.5.0",
|
"@lingui/vite-plugin": "^6.5.0",
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.5.4",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.3.0",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.3.0",
|
||||||
"@types/web-push": "^3.6.4",
|
"@types/web-push": "^3.6.4",
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
|||||||
@@ -50,7 +50,86 @@ textarea,
|
|||||||
transform: scale(0.90) translateY(-4px);
|
transform: scale(0.90) translateY(-4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root),
|
||||||
|
::view-transition-old(page),
|
||||||
|
::view-transition-new(page),
|
||||||
|
::view-transition-old(app-header),
|
||||||
|
::view-transition-new(app-header) {
|
||||||
|
animation-duration: 180ms;
|
||||||
|
animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-nav-direction="forward"]::view-transition-old(page) {
|
||||||
|
animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-under-out;
|
||||||
|
}
|
||||||
|
html[data-nav-direction="forward"]::view-transition-new(page) {
|
||||||
|
animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-over-in;
|
||||||
|
}
|
||||||
|
html[data-nav-direction="back"]::view-transition-old(page) {
|
||||||
|
animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-over-out;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
html[data-nav-direction="back"]::view-transition-new(page) {
|
||||||
|
animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-under-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-nav-direction="forward"][data-header-vt="exit"]::view-transition-old(app-header) {
|
||||||
|
animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-header-out;
|
||||||
|
}
|
||||||
|
html[data-nav-direction="back"][data-header-vt="exit"]::view-transition-old(app-header) {
|
||||||
|
animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-over-out;
|
||||||
|
}
|
||||||
|
html[data-nav-direction="forward"][data-header-vt="enter"]::view-transition-new(app-header) {
|
||||||
|
animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-page-over-in;
|
||||||
|
}
|
||||||
|
html[data-nav-direction="back"][data-header-vt="enter"]::view-transition-new(app-header) {
|
||||||
|
animation: 300ms cubic-bezier(0.32, 0.72, 0, 1) both flxn-header-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flxn-page-over-in {
|
||||||
|
from { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
@keyframes flxn-page-over-out {
|
||||||
|
to { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
@keyframes flxn-page-under-out {
|
||||||
|
to { transform: translateX(-25%); opacity: 0.8; }
|
||||||
|
}
|
||||||
|
@keyframes flxn-page-under-in {
|
||||||
|
from { transform: translateX(-25%); opacity: 0.8; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flxn-header-out {
|
||||||
|
45% { opacity: 0; }
|
||||||
|
to { transform: translateX(-25%); opacity: 0; }
|
||||||
|
}
|
||||||
|
@keyframes flxn-header-in {
|
||||||
|
from { transform: translateX(-25%); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flxn-vt-fade-out {
|
||||||
|
to { opacity: 0; }
|
||||||
|
}
|
||||||
|
@keyframes flxn-vt-fade-in {
|
||||||
|
from { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
::view-transition-group(*),
|
||||||
|
::view-transition-image-pair(*) {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-old(*) {
|
||||||
|
animation: 120ms ease both flxn-vt-fade-out !important;
|
||||||
|
z-index: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(*) {
|
||||||
|
animation: 120ms ease both flxn-vt-fade-in !important;
|
||||||
|
}
|
||||||
|
|
||||||
.app,
|
.app,
|
||||||
[data-drawer-level] {
|
[data-drawer-level] {
|
||||||
transition: none;
|
transition: none;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
|||||||
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
|
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
|
||||||
import { routeTree } from "./routeTree.gen";
|
import { routeTree } from "./routeTree.gen";
|
||||||
import { DefaultCatchBoundary } from "../components/DefaultCatchBoundary";
|
import { DefaultCatchBoundary } from "../components/DefaultCatchBoundary";
|
||||||
|
import RoutePending from "../components/route-pending";
|
||||||
|
import { headerChange, navDirection } from "@/features/core/utils/nav-direction";
|
||||||
import { defaultHeaderConfig } from "@/features/core/hooks/use-router-config";
|
import { defaultHeaderConfig } from "@/features/core/hooks/use-router-config";
|
||||||
|
|
||||||
export function getRouter() {
|
export function getRouter() {
|
||||||
@@ -30,6 +32,15 @@ export function getRouter() {
|
|||||||
},
|
},
|
||||||
defaultPreload: "intent",
|
defaultPreload: "intent",
|
||||||
defaultPreloadStaleTime: 60_000,
|
defaultPreloadStaleTime: 60_000,
|
||||||
|
defaultPendingMs: 100,
|
||||||
|
defaultPendingMinMs: 0,
|
||||||
|
defaultViewTransition: {
|
||||||
|
types: ({ fromLocation, toLocation }) => {
|
||||||
|
const direction = navDirection(fromLocation, toLocation);
|
||||||
|
return direction === "none" ? false : [direction];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultPendingComponent: RoutePending,
|
||||||
defaultErrorComponent: DefaultCatchBoundary,
|
defaultErrorComponent: DefaultCatchBoundary,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,6 +49,13 @@ export function getRouter() {
|
|||||||
queryClient
|
queryClient
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
router.subscribe("onBeforeNavigate", ({ fromLocation, toLocation }) => {
|
||||||
|
document.documentElement.dataset.navDirection = navDirection(fromLocation, toLocation);
|
||||||
|
document.documentElement.dataset.headerVt = headerChange(fromLocation, toLocation);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { tournamentQueries, useCurrentTournament } from "@/features/tournaments/queries";
|
import { tournamentQueries, useCurrentTournament } from "@/features/tournaments/queries";
|
||||||
import UpcomingTournament from "@/features/tournaments/components/upcoming-tournament";
|
import UpcomingTournament from "@/features/tournaments/components/upcoming-tournament";
|
||||||
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
import { getServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||||
|
import { prefetchServerQuery } from "@/lib/tanstack-query/utils/prefetch";
|
||||||
import StartedTournament from "@/features/tournaments/components/started-tournament";
|
import StartedTournament from "@/features/tournaments/components/started-tournament";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import UpcomingTournamentSkeleton from "@/features/tournaments/components/upcoming-tournament/skeleton";
|
import UpcomingTournamentSkeleton from "@/features/tournaments/components/upcoming-tournament/skeleton";
|
||||||
@@ -10,16 +11,13 @@ export const Route = createFileRoute("/_authed/")({
|
|||||||
component: () => <Suspense fallback={<UpcomingTournamentSkeleton />}>
|
component: () => <Suspense fallback={<UpcomingTournamentSkeleton />}>
|
||||||
<Home />
|
<Home />
|
||||||
</Suspense>,
|
</Suspense>,
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: ({ context }) => {
|
||||||
const queryClient = context.queryClient;
|
prefetchServerQuery(context.queryClient, tournamentQueries.current());
|
||||||
const tournament = await ensureServerQueryData(queryClient, tournamentQueries.current())
|
|
||||||
|
|
||||||
return { tournament }
|
|
||||||
},
|
},
|
||||||
loader: ({ context }) => ({
|
loader: ({ context }) => ({
|
||||||
withPadding: false,
|
withPadding: false,
|
||||||
header: {
|
header: {
|
||||||
title: context.tournament.name || "FLXN"
|
title: getServerQueryData(context.queryClient, tournamentQueries.current())?.name || "FLXN"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
pendingComponent: () => <UpcomingTournamentSkeleton />
|
pendingComponent: () => <UpcomingTournamentSkeleton />
|
||||||
|
|||||||
@@ -14,26 +14,12 @@ const searchSchema = z.object({
|
|||||||
|
|
||||||
export const Route = createFileRoute("/_authed/profile/$playerId")({
|
export const Route = createFileRoute("/_authed/profile/$playerId")({
|
||||||
validateSearch: searchSchema,
|
validateSearch: searchSchema,
|
||||||
beforeLoad: async ({ params, context }) => {
|
beforeLoad: ({ params, context }) => {
|
||||||
const { queryClient } = context;
|
const { queryClient } = context;
|
||||||
await Promise.all([
|
prefetchServerQuery(queryClient, playerQueries.details(params.playerId));
|
||||||
prefetchServerQuery(
|
prefetchServerQuery(queryClient, playerQueries.matches(params.playerId));
|
||||||
queryClient,
|
prefetchServerQuery(queryClient, playerQueries.stats(params.playerId));
|
||||||
playerQueries.details(params.playerId)
|
prefetchServerQuery(queryClient, badgeQueries.playerBadges(params.playerId));
|
||||||
),
|
|
||||||
prefetchServerQuery(
|
|
||||||
queryClient,
|
|
||||||
playerQueries.matches(params.playerId)
|
|
||||||
),
|
|
||||||
prefetchServerQuery(
|
|
||||||
queryClient,
|
|
||||||
playerQueries.stats(params.playerId)
|
|
||||||
),
|
|
||||||
prefetchServerQuery(
|
|
||||||
queryClient,
|
|
||||||
badgeQueries.playerBadges(params.playerId)
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
},
|
},
|
||||||
loader: ({ params, context }) => ({
|
loader: ({ params, context }) => ({
|
||||||
header: {
|
header: {
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ const searchSchema = z.object({
|
|||||||
|
|
||||||
export const Route = createFileRoute("/_authed/teams/$teamId")({
|
export const Route = createFileRoute("/_authed/teams/$teamId")({
|
||||||
validateSearch: searchSchema,
|
validateSearch: searchSchema,
|
||||||
beforeLoad: async ({ params, context }) => {
|
beforeLoad: ({ params, context }) => {
|
||||||
const { queryClient } = context;
|
const { queryClient } = context;
|
||||||
await prefetchServerQuery(queryClient, teamQueries.details(params.teamId));
|
prefetchServerQuery(queryClient, teamQueries.details(params.teamId));
|
||||||
},
|
},
|
||||||
loader: ({ params }) => ({
|
loader: ({ params }) => ({
|
||||||
header: {
|
header: {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Skeleton, Stack } from "@mantine/core";
|
||||||
|
|
||||||
|
const RoutePending = () => (
|
||||||
|
<Stack gap="md" p="md" w="100%">
|
||||||
|
{Array.from({ length: 5 }).map((_, index) => (
|
||||||
|
<Skeleton
|
||||||
|
key={`route-pending-${index}`}
|
||||||
|
height={96}
|
||||||
|
w="100%"
|
||||||
|
radius="md"
|
||||||
|
style={{ opacity: Math.max(1 - index * 0.15, 0.3) }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default RoutePending;
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
ReactNode,
|
ReactNode,
|
||||||
useRef,
|
useRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
startTransition,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useRouter } from "@tanstack/react-router";
|
import { useRouter } from "@tanstack/react-router";
|
||||||
|
|
||||||
@@ -48,6 +48,11 @@ function SwipeableTabs({
|
|||||||
}, [search?.tab, tabs, defaultTab]);
|
}, [search?.tab, tabs, defaultTab]);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState(getActiveTabFromUrl);
|
const [activeTab, setActiveTab] = useState(getActiveTabFromUrl);
|
||||||
|
const [mountAll, setMountAll] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const timeoutId = setTimeout(() => startTransition(() => setMountAll(true)), 400);
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, []);
|
||||||
const [stuck, setStuck] = useState(false);
|
const [stuck, setStuck] = useState(false);
|
||||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||||
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
|
const [rootRef, setRootRef] = useState<HTMLDivElement | null>(null);
|
||||||
@@ -225,7 +230,7 @@ function SwipeableTabs({
|
|||||||
{tabs.map((tab, index) => (
|
{tabs.map((tab, index) => (
|
||||||
<Carousel.Slide key={`${tab.label}-content-${index}`}>
|
<Carousel.Slide key={`${tab.label}-content-${index}`}>
|
||||||
<Box ref={setSlideRef(index)} style={{ height: "auto", width: "100%", maxWidth: "100vw", overflow: "hidden" }}>
|
<Box ref={setSlideRef(index)} style={{ height: "auto", width: "100%", maxWidth: "100vw", overflow: "hidden" }}>
|
||||||
{tab.content}
|
{(mountAll || index === activeTab) ? tab.content : null}
|
||||||
</Box>
|
</Box>
|
||||||
</Carousel.Slide>
|
</Carousel.Slide>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export const BadgeIcon = ({ badge, filled, size = 48 }: BadgeIconProps & { size?
|
|||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
onError={() => setImageError(true)}
|
onError={() => setImageError(true)}
|
||||||
style={{
|
style={{
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const Header = ({ collapsed, title, titleValues, withBackButton }: HeaderConfig)
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingTop: 'env(safe-area-inset-top, 0px)',
|
paddingTop: 'env(safe-area-inset-top, 0px)',
|
||||||
|
viewTransitionName: 'app-header',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ withBackButton && <BackButton /> }
|
{ withBackButton && <BackButton /> }
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
import { AppShell, Box } from '@mantine/core';
|
import { AppShell, Box } from '@mantine/core';
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { useLocation } from '@tanstack/react-router';
|
|
||||||
import Header from './header';
|
import Header from './header';
|
||||||
import Navbar from './navbar';
|
import Navbar from './navbar';
|
||||||
import Pullable from './pullable';
|
import Pullable from './pullable';
|
||||||
import useVisualViewportSize from '../hooks/use-visual-viewport-size';
|
import useVisualViewportSize from '../hooks/use-visual-viewport-size';
|
||||||
import useRouterConfig from '../hooks/use-router-config';
|
import useRouterConfig from '../hooks/use-router-config';
|
||||||
import Page from '@/components/page';
|
import Page from '@/components/page';
|
||||||
import './route-transition.css';
|
|
||||||
|
|
||||||
const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
const { header, withPadding, fullWidth } = useRouterConfig();
|
const { header, withPadding, fullWidth } = useRouterConfig();
|
||||||
const viewport = useVisualViewportSize();
|
const viewport = useVisualViewportSize();
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
id='app-shell'
|
id='app-shell'
|
||||||
layout='alt'
|
layout='alt'
|
||||||
|
// must snap: view transitions capture AppShell's collapse mid-animation
|
||||||
|
transitionDuration={0}
|
||||||
header={{
|
header={{
|
||||||
// Old installs froze black-translucent at add-to-home-screen time, so
|
// Old installs froze black-translucent at add-to-home-screen time, so
|
||||||
// the webview can extend under the status bar; inset is 0 elsewhere.
|
// the webview can extend under the status bar; inset is 0 elsewhere.
|
||||||
@@ -48,18 +47,27 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
|||||||
pos='relative'
|
pos='relative'
|
||||||
h='100%'
|
h='100%'
|
||||||
mah='100%'
|
mah='100%'
|
||||||
pb={{ base: 'calc(65px + env(safe-area-inset-bottom, 0px))', sm: 0 }}
|
pb={{ base: 'calc(73px + env(safe-area-inset-bottom, 0px))', sm: 0 }}
|
||||||
px={{ base: 0.01, sm: 100, md: 200, lg: 300 }}
|
px={{ base: 0.01, sm: 100, md: 200, lg: 300 }}
|
||||||
maw='100dvw'
|
maw='100dvw'
|
||||||
style={{ overflow: 'hidden' }}
|
style={{ overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
<Pullable>
|
{/* pane box must be opaque and route-invariant or slides drift */}
|
||||||
<Page noPadding={!withPadding} fullWidth={fullWidth}>
|
<Box
|
||||||
<Box key={location.pathname} className="flxn-route-enter">
|
style={{
|
||||||
|
viewTransitionName: 'page',
|
||||||
|
backgroundColor: 'var(--mantine-color-body)',
|
||||||
|
marginTop: 'calc(-1 * var(--app-shell-header-offset, 0px))',
|
||||||
|
paddingTop: 'var(--app-shell-header-offset, 0px)',
|
||||||
|
height: 'calc(100% + var(--app-shell-header-offset, 0px))',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pullable>
|
||||||
|
<Page noPadding={!withPadding} fullWidth={fullWidth}>
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Page>
|
||||||
</Page>
|
</Pullable>
|
||||||
</Pullable>
|
</Box>
|
||||||
</AppShell.Main>
|
</AppShell.Main>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<AppShell.Aside withBorder />
|
<AppShell.Aside withBorder />
|
||||||
|
|||||||
@@ -11,11 +11,6 @@
|
|||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navLinkBox svg,
|
|
||||||
.navLinkBox p {
|
|
||||||
transition: color 150ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
.navLinkBox {
|
.navLinkBox {
|
||||||
transition: transform 150ms cubic-bezier(0.32, 0.72, 0, 1);
|
transition: transform 150ms cubic-bezier(0.32, 0.72, 0, 1);
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.flxn-route-enter {
|
|
||||||
animation: flxn-route-fade-in 180ms ease-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes flxn-route-fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { ParsedLocation } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export type NavDirection = "forward" | "back" | "none";
|
||||||
|
|
||||||
|
const tabRank = (pathname: string): number | null => {
|
||||||
|
if (pathname === "/") return 0;
|
||||||
|
if (pathname.startsWith("/stats")) return 1;
|
||||||
|
if (pathname.startsWith("/tournaments")) return 2;
|
||||||
|
if (pathname.startsWith("/profile") || pathname.startsWith("/settings")) return 3;
|
||||||
|
if (pathname.startsWith("/admin")) return 4;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const depth = (pathname: string) => pathname.split("/").filter(Boolean).length;
|
||||||
|
|
||||||
|
// Keep in sync with route loaders that set header.collapsed
|
||||||
|
const headerHidden = (pathname: string) => {
|
||||||
|
const segs = pathname.split("/").filter(Boolean);
|
||||||
|
return segs.length === 2 && ["profile", "teams", "tournaments"].includes(segs[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function headerChange(
|
||||||
|
from: ParsedLocation | undefined,
|
||||||
|
to: ParsedLocation
|
||||||
|
): "exit" | "enter" | "none" {
|
||||||
|
if (!from) return "none";
|
||||||
|
const fromHidden = headerHidden(from.pathname);
|
||||||
|
const toHidden = headerHidden(to.pathname);
|
||||||
|
if (fromHidden === toHidden) return "none";
|
||||||
|
return toHidden ? "exit" : "enter";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function navDirection(
|
||||||
|
from: ParsedLocation | undefined,
|
||||||
|
to: ParsedLocation
|
||||||
|
): NavDirection {
|
||||||
|
if (!from || from.pathname === to.pathname) return "none";
|
||||||
|
|
||||||
|
const fromRank = tabRank(from.pathname);
|
||||||
|
const toRank = tabRank(to.pathname);
|
||||||
|
if (fromRank !== null && toRank !== null && fromRank !== toRank) {
|
||||||
|
return toRank > fromRank ? "forward" : "back";
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromIndex = from.state.__TSR_index;
|
||||||
|
const toIndex = to.state.__TSR_index;
|
||||||
|
if (Number.isFinite(fromIndex) && Number.isFinite(toIndex) && fromIndex !== toIndex) {
|
||||||
|
return toIndex > fromIndex ? "forward" : "back";
|
||||||
|
}
|
||||||
|
|
||||||
|
return depth(to.pathname) >= depth(from.pathname) ? "forward" : "back";
|
||||||
|
}
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
import { QueryClient, QueryKey } from "@tanstack/react-query";
|
import { QueryClient, QueryKey } from "@tanstack/react-query";
|
||||||
import { ServerResult } from "../types";
|
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>(
|
export async function ensureServerQueryData<TData>(
|
||||||
queryClient: QueryClient,
|
queryClient: QueryClient,
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
@@ -2091,7 +2091,7 @@ msgstr "Vorheriger Titel"
|
|||||||
msgid "Profile"
|
msgid "Profile"
|
||||||
msgstr "Profil"
|
msgstr "Profil"
|
||||||
|
|
||||||
#: src/features/badges/components/badge-showcase.tsx:301
|
#: src/features/badges/components/badge-showcase.tsx:302
|
||||||
msgid "Progress "
|
msgid "Progress "
|
||||||
msgstr "Fortschritt "
|
msgstr "Fortschritt "
|
||||||
|
|
||||||
|
|||||||
@@ -2091,7 +2091,7 @@ msgstr "Previous track"
|
|||||||
msgid "Profile"
|
msgid "Profile"
|
||||||
msgstr "Profile"
|
msgstr "Profile"
|
||||||
|
|
||||||
#: src/features/badges/components/badge-showcase.tsx:301
|
#: src/features/badges/components/badge-showcase.tsx:302
|
||||||
msgid "Progress "
|
msgid "Progress "
|
||||||
msgstr "Progress "
|
msgstr "Progress "
|
||||||
|
|
||||||
|
|||||||
@@ -2091,7 +2091,7 @@ msgstr "Pista anterior"
|
|||||||
msgid "Profile"
|
msgid "Profile"
|
||||||
msgstr "Perfil"
|
msgstr "Perfil"
|
||||||
|
|
||||||
#: src/features/badges/components/badge-showcase.tsx:301
|
#: src/features/badges/components/badge-showcase.tsx:302
|
||||||
msgid "Progress "
|
msgid "Progress "
|
||||||
msgstr "Progreso "
|
msgstr "Progreso "
|
||||||
|
|
||||||
|
|||||||
@@ -2091,7 +2091,7 @@ msgstr "前のトラック"
|
|||||||
msgid "Profile"
|
msgid "Profile"
|
||||||
msgstr "プロフィール"
|
msgstr "プロフィール"
|
||||||
|
|
||||||
#: src/features/badges/components/badge-showcase.tsx:301
|
#: src/features/badges/components/badge-showcase.tsx:302
|
||||||
msgid "Progress "
|
msgid "Progress "
|
||||||
msgstr "進行状況 "
|
msgstr "進行状況 "
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user