refactor: page transitioning

This commit is contained in:
Sonny
2025-12-10 20:08:50 +01:00
parent b119b29ad1
commit e56bc297fd
9 changed files with 90 additions and 62 deletions

View File

@@ -25,34 +25,18 @@
--mantine-color-body: var(--ml-bg-dark) !important;
}
.__transition_fadeIn {
transform-origin: 50% top;
animation: fadeIn 0.15s ease both;
}
.__transition_fadeOut {
transform-origin: 50% top;
animation: fadeOut 0.15s ease both;
}
@keyframes fadeIn {
@keyframes pageFadeIn {
from {
opacity: 0;
transform: scale(0.9);
filter: blur(0.44rem);
}
to {
opacity: 1;
transform: none;
filter: blur(0);
}
}
@keyframes fadeOut {
from {
opacity: 1;
transform: none;
}
to {
opacity: 0;
transform: scale(0.9);
}
.page-transition-enter {
animation: pageFadeIn 250ms ease forwards;
will-change: opacity, filter;
}

View File

@@ -0,0 +1,54 @@
import { useEffect, useRef } from 'react';
import {
getCurrentPathAndSearch,
getPathAndSearchFromRaw,
restartCssAnimation,
} from '~/lib/navigation';
import { InertiaSuccessEvent } from '~/types/inertia';
const PAGE_TRANSITION_CLASS = 'page-transition-enter';
interface UsePageTransitionProps {
querySelector: string;
}
export const usePageTransition = ({
querySelector,
}: UsePageTransitionProps): void => {
const previousUrlRef = useRef<string>(getCurrentPathAndSearch());
useEffect(() => {
const handleSuccess = (event: Event) => {
const element = document.querySelector(
querySelector
) as HTMLElement | null;
if (!element) return;
const { detail } = event as InertiaSuccessEvent;
const nextUrlRaw = detail?.page?.url ?? getCurrentPathAndSearch();
const next = getPathAndSearchFromRaw(nextUrlRaw);
const prev = previousUrlRef.current;
if (next === prev) return;
previousUrlRef.current = next;
restartCssAnimation(element, PAGE_TRANSITION_CLASS);
const onEnd = () => {
element.classList.remove(PAGE_TRANSITION_CLASS);
element.removeEventListener('animationend', onEnd);
};
element.addEventListener('animationend', onEnd);
};
document.addEventListener(
'inertia:success',
handleSuccess as EventListener
);
return () =>
document.removeEventListener(
'inertia:success',
handleSuccess as EventListener
);
}, [querySelector]);
};

View File

@@ -1,6 +1,5 @@
import { api } from '#adonis/api';
import { PRIMARY_COLOR } from '#config/project';
import { router } from '@inertiajs/react';
import {
ColorSchemeScript,
createTheme,
@@ -13,14 +12,12 @@ import '@mantine/spotlight/styles.css';
import { createTuyau } from '@tuyau/client';
import { TuyauProvider } from '@tuyau/inertia/react';
import dayjs from 'dayjs';
import { ReactNode, useEffect } from 'react';
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import 'virtual:uno.css';
import '~/css/app.css';
import { useAppUrl } from '~/hooks/use_app_url';
const TRANSITION_IN_CLASS = '__transition_fadeIn';
const TRANSITION_OUT_CLASS = '__transition_fadeOut';
import { usePageTransition } from '~/hooks/use_page_transition';
const customTheme = createTheme({
colors: {
@@ -78,46 +75,13 @@ export function BaseLayout({ children }: { children: ReactNode }) {
const appUrl = useAppUrl();
dayjs.locale(i18n.language);
usePageTransition({ querySelector: '#app > div:nth-child(5)' });
const tuyauClient = createTuyau({
api,
baseUrl: appUrl,
});
const findAppElement = () => document.getElementById('app');
const flipClass = (addClass: string, removeClass: string) => {
const appElement = findAppElement();
if (appElement) {
appElement.classList.add(addClass);
appElement.classList.remove(removeClass);
}
};
const canTransition = (currentLocation: URL, newLocation: URL) =>
currentLocation.pathname !== newLocation.pathname;
useEffect(() => {
const currentLocation = new URL(window.location.href);
const removeStartEventListener = router.on(
'start',
(event) =>
canTransition(currentLocation, event.detail.visit.url) &&
flipClass(TRANSITION_OUT_CLASS, TRANSITION_IN_CLASS)
);
const removefinishEventListener = router.on(
'finish',
(event) =>
canTransition(currentLocation, event.detail.visit.url) &&
flipClass(TRANSITION_IN_CLASS, TRANSITION_OUT_CLASS)
);
return () => {
removeStartEventListener();
removefinishEventListener();
};
}, []);
return (
<TuyauProvider client={tuyauClient}>
<ColorSchemeScript />

View File

@@ -77,3 +77,27 @@ export const buildUrl = (url: string, params: Record<string, string>) => {
});
return urlObj.toString();
};
export function getCurrentPathAndSearch(): string {
if (typeof window === 'undefined') return '';
return `${window.location.pathname}${window.location.search}`;
}
export function getPathAndSearchFromRaw(rawUrl: string): string {
try {
const url = new URL(rawUrl, window.location.origin);
return `${url.pathname}${url.search}`;
} catch {
return rawUrl;
}
}
export function restartCssAnimation(
element: HTMLElement,
className: string
): void {
element.classList.remove(className);
// Force reflow to ensure animation restarts when re-adding the class
void element.offsetWidth;
element.classList.add(className);
}

View File

@@ -15,3 +15,5 @@ export type InertiaPage<
> = T & {
auth: Auth;
};
export type InertiaSuccessEvent = CustomEvent<InertiaSuccessDetail>;