Skip to content

Commit

Permalink
Merge pull request #142 from Gurubase/task/fix-loading-animation-comp…
Browse files Browse the repository at this point in the history
…letion

Task/fix loading animation completion
  • Loading branch information
kursataktas authored Feb 25, 2025
2 parents 769d80c + 68ec897 commit 518ef62
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 98 deletions.
7 changes: 5 additions & 2 deletions src/gurubase-frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -936,14 +936,17 @@ input[data-follow-up-input] {
}

@keyframes progress {
0% {
width: 0%;
}
5% {
width: 5%;
}
40% {
width: 40%;
}
100% {
width: 98%;
width: 80%;
}
}

Expand All @@ -954,7 +957,7 @@ input[data-follow-up-input] {
}

.animate-progress {
animation: progress 2s ease-in-out infinite;
animation: progress 2s ease-in-out forwards;
}

.anteon-prose
Expand Down
30 changes: 17 additions & 13 deletions src/gurubase-frontend/src/components/BingeMap/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,8 @@ export function BingeMap({
setPan(optimalPan);

if (isDesktop) {
if (clickedNode.slug) {
// Skip handleQuestionUpdate if the clicked node is already the current question
if (clickedNode.slug && clickedNode.slug !== currentQuestionSlug) {
dispatch(setIsBingeMapOpen(false));
dispatch(setInputQuery(""));

Expand Down Expand Up @@ -508,24 +509,27 @@ export function BingeMap({
};

const handleNodeSelect = async (selectedNode) => {
// Skip handleQuestionUpdate if the selected node is already the current question
dispatch(setIsBingeMapOpen(false));
setPan({ x: 0, y: 0 });
setActiveNode(null);
setHoveredNode(null);
dispatch(setInputQuery(""));

await handleQuestionUpdate({
guruType,
newSlug: selectedNode.slug,
oldSlug: currentQuestionSlug,
treeData,
dispatch,
setContent,
setQuestion,
setDescription,
bingeId,
questionText
});
if (selectedNode.slug !== currentQuestionSlug) {
await handleQuestionUpdate({
guruType,
newSlug: selectedNode.slug,
oldSlug: currentQuestionSlug,
treeData,
dispatch,
setContent,
setQuestion,
setDescription,
bingeId,
questionText
});
}
};

const handleZoomIn = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React from "react";

import { SettingsIcon } from "@/components/Icons";
import { Link } from "@/components/Link";
import { useAppNavigation } from "@/lib/navigation";
import { useAppDispatch } from "@/redux/hooks";
import { resetErrors, setResetMainForm } from "@/redux/slices/mainFormSlice";
Expand Down
10 changes: 6 additions & 4 deletions src/gurubase-frontend/src/components/PageTransition.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { useAppSelector } from "@/redux/hooks";
import { useState, useEffect, useRef } from "react";
import { useNavigation } from "@/hooks/useNavigation";
import { useAppDispatch } from "@/redux/hooks";

export const PageTransition = () => {
const isNavigating = useNavigation((state) => state.isNavigating);
Expand All @@ -16,30 +15,33 @@ export const PageTransition = () => {
if (isNavigating || isPageTransitioning) {
setIsVisible(true);
} else if (isVisible && progressRef.current) {
// Stop the current animation and keep it on the last frame
const element = progressRef.current;
const computedStyle = window.getComputedStyle(element);
const currentWidth = computedStyle.getPropertyValue("width");

// Stop the progress animation
element.style.animation = "none";
element.style.width = currentWidth;

// Necessary for reflow
element.offsetHeight;

// Start the completion animation
element.style.animation = "progress-to-complete 500ms ease-out forwards";
element.style.animation = "progress-to-complete 300ms ease-out forwards";

// Hide when animation ends
element.addEventListener(
"animationend",
() => {
setIsVisible(false);
// Reset the animation state
element.style.animation = "";
element.style.width = "0%";
},
{ once: true }
);
}
}, [isNavigating, isPageTransitioning]);
}, [isNavigating, isPageTransitioning, isVisible]);

if (!isVisible) return null;

Expand Down
6 changes: 5 additions & 1 deletion src/gurubase-frontend/src/hooks/useNavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ import { create } from "zustand";
export const useNavigation = create((set) => ({
isNavigating: false,
startNavigation: () => set({ isNavigating: true }),
endNavigation: () => set({ isNavigating: false })
endNavigation: () => set({ isNavigating: false }),
handleNavigationComplete: () => {
// Small delay to ensure any route data is loaded
setTimeout(() => set({ isNavigating: false }), 100);
}
}));
148 changes: 71 additions & 77 deletions src/gurubase-frontend/src/lib/navigation.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useRouter } from "next/navigation";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";

import { useNavigation } from "@/hooks/useNavigation";

Expand All @@ -27,108 +28,101 @@ import { useNavigation } from "@/hooks/useNavigation";
// };

// Create a singleton instance for the navigation state
let navigationInstance = null;
// UNUSED:
// let navigationInstance = null;

export const getNavigation = () => {
if (!navigationInstance) {
const router = useRouter();
const { startNavigation, endNavigation } = useNavigation();
// export const getNavigation = () => {
// if (!navigationInstance) {
// const router = useRouter();
// const { startNavigation, handleNavigationComplete } = useNavigation();

navigationInstance = {
push: async (path) => {
startNavigation();
try {
router.push(path);
} finally {
setTimeout(endNavigation, 1000);
}
},
replace: async (path) => {
startNavigation();
try {
router.replace(path);
} finally {
setTimeout(endNavigation, 1000);
}
},
back: () => {
startNavigation();
try {
router.back();
} finally {
setTimeout(endNavigation, 1000);
}
},
setHref: (url) => {
startNavigation();
// Don't need to end navigation as the page will reload
window.location.href = url;
},
pushState: (state, title, url) => {
startNavigation();
window.history.pushState(state, title, url);
setTimeout(endNavigation, 1000);
}
};
}
// navigationInstance = {
// push: async (path) => {
// startNavigation();
// router.push(path);
// },
// replace: async (path) => {
// startNavigation();
// router.replace(path);
// },
// back: () => {
// startNavigation();
// router.back();
// },
// setHref: (url) => {
// startNavigation();
// // Don't need to end navigation as the page will reload
// window.location.href = url;
// },
// pushState: (state, title, url) => {
// startNavigation();
// window.history.pushState(state, title, url);
// handleNavigationComplete();
// }
// };
// }

return navigationInstance;
};
// return navigationInstance;
// };

// Hook version for use within components
export const useAppNavigation = () => {
const router = useRouter();
const { startNavigation, endNavigation } = useNavigation();
const pathname = usePathname();
const searchParams = useSearchParams();
const { startNavigation, handleNavigationComplete } = useNavigation();

// Listen for route changes
useEffect(() => {
handleNavigationComplete();
}, [pathname, searchParams, handleNavigationComplete]);

// Helper to normalize paths for comparison
const normalizePath = (path) => {
// Remove trailing slash if present (except for root path)
return path === "/" ? path : path.replace(/\/$/, "");
};

// Helper to check if paths are the same
const isSamePath = (targetPath) => {
// Get current URL with query params
const currentFullPath =
pathname + (searchParams.toString() ? `?${searchParams.toString()}` : "");

// Normalize both paths
const normalizedCurrentPath = normalizePath(currentFullPath);
const normalizedTargetPath = normalizePath(targetPath);

return normalizedCurrentPath === normalizedTargetPath;
};

return {
push: async (path) => {
if (!useNavigation.getState().isNavigating) {
// Only start if not already navigating
// Don't start navigation if already navigating or if it's the same path
if (!useNavigation.getState().isNavigating && !isSamePath(path)) {
startNavigation();
try {
router.push(path);
} finally {
// Increased delay to ensure the animation completes smoothly
setTimeout(endNavigation, 1000);
}
router.push(path);
}
},
replace: async (path) => {
if (!useNavigation.getState().isNavigating) {
// Only start if not already navigating
// Don't start navigation if already navigating or if it's the same path
if (!useNavigation.getState().isNavigating && !isSamePath(path)) {
startNavigation();
try {
router.replace(path);
} finally {
// Increased delay to ensure the animation completes smoothly
setTimeout(endNavigation, 1000);
}
router.replace(path);
}
},
back: () => {
if (!useNavigation.getState().isNavigating) {
// Only start if not already navigating
startNavigation();
try {
router.back();
} finally {
// Increased delay to ensure the animation completes smoothly
setTimeout(endNavigation, 1000);
}
router.back();
}
},
setHref: (url) => {
if (!useNavigation.getState().isNavigating) {
// Only start if not already navigating
// Don't start navigation if already navigating or if it's the same URL
if (!useNavigation.getState().isNavigating && !isSamePath(url)) {
startNavigation();
// Don't need to end navigation as the page will reload
window.location.href = url;
}
},
pushState: (state, title, url) => {
startNavigation();
window.history.pushState(state, title, url);
setTimeout(endNavigation, 1000);
}
};
};

0 comments on commit 518ef62

Please sign in to comment.